Compare commits

...

74 Commits
v1.1.3 ... main

Author SHA1 Message Date
Simon Bouchard a6fab4dc75
feat: update strategy for reinstall (#2019)
* feat: Add updateStrategy option in the state file with 'reinstall'/'reinstallIfForbidden' choices to uninstall and apply the specific release(s) (if forbidden to update)

Signed-off-by: Simon Bouchard <sbouchard@rbbn.com>

* Fix unit tests related to the new updateStrategy feature

Signed-off-by: Simon Bouchard <sbouchard@rbbn.com>

* Fix unit tests related to the new updateStrategy feature

Signed-off-by: Simon Bouchard <sbouchard@rbbn.com>

* Resolve linter issue due to cognitive complexity

Signed-off-by: Simon Bouchard <sbouchard@rbbn.com>

* Updated index.md to describe the possible values of updateStrategy

Signed-off-by: Simon Bouchard <sbouchard@rbbn.com>

* Add validation of updateStrategy parameter and unit test

Signed-off-by: Simon Bouchard <sbouchard@rbbn.com>

* Updated unit test

Signed-off-by: Simon Bouchard <sbouchard@rbbn.com>

* Removed 'reinstall' update strategy option to only have reinstallIfForbidden, cleanup of pre-sync changes, adapted unit tests

Signed-off-by: Simon Bouchard <sbouchard@rbbn.com>

* Display affected releases that were reinstalled

Signed-off-by: Simon Bouchard <sbouchard@rbbn.com>

* Make sure to add --wait when deleting a release to be reinstalled due to reinstallIfForbidden

Signed-off-by: Simon Bouchard <sbouchard@rbbn.com>

* Apply suggestions from Copilot code review

Signed-off-by: Simon Bouchard <sbouchard@rbbn.com>

---------

Signed-off-by: Simon Bouchard <sbouchard@rbbn.com>
2025-10-29 08:47:46 +08:00
dependabot[bot] a45d681a08
build(deps): bump actions/download-artifact from 5 to 6 (#2235)
Bumps [actions/download-artifact](https://github.com/actions/download-artifact) from 5 to 6.
- [Release notes](https://github.com/actions/download-artifact/releases)
- [Commits](https://github.com/actions/download-artifact/compare/v5...v6)

---
updated-dependencies:
- dependency-name: actions/download-artifact
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-28 07:29:24 +08:00
dependabot[bot] 1c8e3d087d
build(deps): bump actions/upload-artifact from 4 to 5 (#2236)
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 4 to 5.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-28 07:29:03 +08:00
Copilot d3908e6a3c
Fix helmBinary and kustomizeBinary being ignored when using bases (#2228)
* Fix helmBinary and kustomizeBinary being ignored when using bases

- Add mergo.WithOverride to merge operations for proper precedence
- Move default binary setting after base loading
- Add comprehensive tests for various base scenarios

Co-authored-by: yxxhero <11087727+yxxhero@users.noreply.github.com>

* Fix code formatting in create_test.go

Co-authored-by: yxxhero <11087727+yxxhero@users.noreply.github.com>

* Remove duplicate comment block in create_test.go

Removed duplicate comment lines (530-532) as identified by code review.

Co-authored-by: yxxhero <11087727+yxxhero@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: yxxhero <11087727+yxxhero@users.noreply.github.com>
2025-10-26 19:41:39 -04:00
dependabot[bot] daebbfb0ad
build(deps): bump github.com/aws/aws-sdk-go-v2/config from 1.31.13 to 1.31.15 (#2233)
build(deps): bump github.com/aws/aws-sdk-go-v2/config

Bumps [github.com/aws/aws-sdk-go-v2/config](https://github.com/aws/aws-sdk-go-v2) from 1.31.13 to 1.31.15.
- [Release notes](https://github.com/aws/aws-sdk-go-v2/releases)
- [Changelog](https://github.com/aws/aws-sdk-go-v2/blob/main/changelog-template.json)
- [Commits](https://github.com/aws/aws-sdk-go-v2/compare/config/v1.31.13...config/v1.31.15)

---
updated-dependencies:
- dependency-name: github.com/aws/aws-sdk-go-v2/config
  dependency-version: 1.31.15
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-26 10:16:26 +08:00
dependabot[bot] 8034acff6e
build(deps): bump github.com/aws/aws-sdk-go-v2/service/s3 from 1.88.6 to 1.88.7 (#2232)
build(deps): bump github.com/aws/aws-sdk-go-v2/service/s3

Bumps [github.com/aws/aws-sdk-go-v2/service/s3](https://github.com/aws/aws-sdk-go-v2) from 1.88.6 to 1.88.7.
- [Release notes](https://github.com/aws/aws-sdk-go-v2/releases)
- [Changelog](https://github.com/aws/aws-sdk-go-v2/blob/main/changelog-template.json)
- [Commits](https://github.com/aws/aws-sdk-go-v2/compare/service/s3/v1.88.6...service/s3/v1.88.7)

---
updated-dependencies:
- dependency-name: github.com/aws/aws-sdk-go-v2/service/s3
  dependency-version: 1.88.7
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-25 08:45:32 +08:00
dependabot[bot] 55adae872e
build(deps): bump github.com/aws/aws-sdk-go-v2/service/s3 from 1.88.5 to 1.88.6 (#2230)
build(deps): bump github.com/aws/aws-sdk-go-v2/service/s3

Bumps [github.com/aws/aws-sdk-go-v2/service/s3](https://github.com/aws/aws-sdk-go-v2) from 1.88.5 to 1.88.6.
- [Release notes](https://github.com/aws/aws-sdk-go-v2/releases)
- [Changelog](https://github.com/aws/aws-sdk-go-v2/blob/main/changelog-template.json)
- [Commits](https://github.com/aws/aws-sdk-go-v2/compare/service/s3/v1.88.5...service/s3/v1.88.6)

---
updated-dependencies:
- dependency-name: github.com/aws/aws-sdk-go-v2/service/s3
  dependency-version: 1.88.6
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-24 09:13:44 +08:00
dependabot[bot] 4fdc4affae
build(deps): bump github.com/aws/aws-sdk-go-v2/config from 1.31.12 to 1.31.13 (#2225)
build(deps): bump github.com/aws/aws-sdk-go-v2/config

Bumps [github.com/aws/aws-sdk-go-v2/config](https://github.com/aws/aws-sdk-go-v2) from 1.31.12 to 1.31.13.
- [Release notes](https://github.com/aws/aws-sdk-go-v2/releases)
- [Changelog](https://github.com/aws/aws-sdk-go-v2/blob/main/changelog-template.json)
- [Commits](https://github.com/aws/aws-sdk-go-v2/compare/config/v1.31.12...config/v1.31.13)

---
updated-dependencies:
- dependency-name: github.com/aws/aws-sdk-go-v2/config
  dependency-version: 1.31.13
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-19 07:23:23 +08:00
dependabot[bot] fc54ff76d2
build(deps): bump github.com/aws/aws-sdk-go-v2/service/s3 from 1.88.4 to 1.88.5 (#2226)
build(deps): bump github.com/aws/aws-sdk-go-v2/service/s3

Bumps [github.com/aws/aws-sdk-go-v2/service/s3](https://github.com/aws/aws-sdk-go-v2) from 1.88.4 to 1.88.5.
- [Release notes](https://github.com/aws/aws-sdk-go-v2/releases)
- [Changelog](https://github.com/aws/aws-sdk-go-v2/blob/main/changelog-template.json)
- [Commits](https://github.com/aws/aws-sdk-go-v2/compare/service/s3/v1.88.4...service/s3/v1.88.5)

---
updated-dependencies:
- dependency-name: github.com/aws/aws-sdk-go-v2/service/s3
  dependency-version: 1.88.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-18 12:00:21 +08:00
Copilot 377ca5c1a2
Bump helm-diff to v3.13.1 (#2223)
* Initial plan

* Bump helm-diff to v3.13.1

Co-authored-by: yxxhero <11087727+yxxhero@users.noreply.github.com>

* Update Dockerfiles to use helm-diff v3.13.1

Co-authored-by: yxxhero <11087727+yxxhero@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: yxxhero <11087727+yxxhero@users.noreply.github.com>
2025-10-16 17:55:49 +08:00
yxxhero 160753c87f
docs: add zread badge to README (#2219)
Signed-off-by: yxxhero <aiopsclub@163.com>
2025-10-13 14:10:00 +08:00
dependabot[bot] ff60d0b565
build(deps): bump github.com/helmfile/vals from 0.42.2 to 0.42.4 (#2217) 2025-10-11 10:46:05 +08:00
Copilot 391c677058
Avoid fetching same chart/version multiple times (#2197)
* Initial plan

* Implement chart fetch deduplication mechanism

Co-authored-by: yxxhero <11087727+yxxhero@users.noreply.github.com>

* Fix CI issues: resolve gci formatting and reduce cognitive complexity

Co-authored-by: yxxhero <11087727+yxxhero@users.noreply.github.com>

* Remove redundant Fetching log message from OCI chart processing

Co-authored-by: yxxhero <11087727+yxxhero@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: yxxhero <11087727+yxxhero@users.noreply.github.com>
2025-10-10 12:13:29 +08:00
dependabot[bot] 310cdead2e
build(deps): bump golang.org/x/term from 0.35.0 to 0.36.0 (#2214) 2025-10-09 08:36:00 +08:00
dependabot[bot] c2d783e872
build(deps): bump github.com/aws/aws-sdk-go-v2/service/s3 from 1.88.3 to 1.88.4 (#2213) 2025-10-09 08:35:27 +08:00
dependabot[bot] 98d9cf4b28
build(deps): bump github.com/hashicorp/go-getter from 1.8.1 to 1.8.2 (#2210)
Bumps [github.com/hashicorp/go-getter](https://github.com/hashicorp/go-getter) from 1.8.1 to 1.8.2.
- [Release notes](https://github.com/hashicorp/go-getter/releases)
- [Changelog](https://github.com/hashicorp/go-getter/blob/main/.goreleaser.yml)
- [Commits](https://github.com/hashicorp/go-getter/compare/v1.8.1...v1.8.2)

---
updated-dependencies:
- dependency-name: github.com/hashicorp/go-getter
  dependency-version: 1.8.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-05 07:43:29 +08:00
Shane Starcher 6673ebad84
fix: skip chartify for build command jsonPatches (#2212)
The build command is intended to be a read-only inspection command that
outputs the helmfile state. However, when releases use jsonPatches,
strategicMergePatches, or transformers, the chart preparation step
triggers chartify, which runs helm template and requires dependencies to
be built.

This causes two issues:
1. helm template is executed unnecessarily for a simple state inspection
2. Missing chart dependencies cause errors even with SkipDeps enabled

This change modifies PrepareCharts to filter out releases that require
chartify when the command is "build". These releases are excluded from
chart preparation, preventing helm template from being invoked.

The state output will still include these releases, but their charts
won't be processed during the build operation.

Signed-off-by: Shane Starcher <shanestarcher@gmail.com>
2025-10-05 07:39:54 +08:00
Ori Shamir 1b8f2871f6
Add yq to Dockerfile (#2208)
Signed-off-by: Ori Shamir <orishamir04@gmail.com>
2025-10-01 21:51:45 +08:00
dependabot[bot] e34ea571fc
build(deps): bump github.com/aws/aws-sdk-go-v2/config from 1.31.10 to 1.31.12 (#2207)
build(deps): bump github.com/aws/aws-sdk-go-v2/config

Bumps [github.com/aws/aws-sdk-go-v2/config](https://github.com/aws/aws-sdk-go-v2) from 1.31.10 to 1.31.12.
- [Release notes](https://github.com/aws/aws-sdk-go-v2/releases)
- [Changelog](https://github.com/aws/aws-sdk-go-v2/blob/main/changelog-template.json)
- [Commits](https://github.com/aws/aws-sdk-go-v2/compare/config/v1.31.10...config/v1.31.12)

---
updated-dependencies:
- dependency-name: github.com/aws/aws-sdk-go-v2/config
  dependency-version: 1.31.12
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-01 11:57:46 +08:00
Ori Shamir 70205ac9ce
Bump Dockerfile to alpine 3.22 (#2205)
Bump Alpine to 3.22

Signed-off-by: Ori Shamir <orishamir04@gmail.com>
2025-10-01 11:34:32 +08:00
dependabot[bot] 3a5c57e144
build(deps): bump github.com/aws/aws-sdk-go-v2/service/s3 from 1.88.2 to 1.88.3 (#2206)
build(deps): bump github.com/aws/aws-sdk-go-v2/service/s3

Bumps [github.com/aws/aws-sdk-go-v2/service/s3](https://github.com/aws/aws-sdk-go-v2) from 1.88.2 to 1.88.3.
- [Release notes](https://github.com/aws/aws-sdk-go-v2/releases)
- [Changelog](https://github.com/aws/aws-sdk-go-v2/blob/main/changelog-template.json)
- [Commits](https://github.com/aws/aws-sdk-go-v2/compare/service/s3/v1.88.2...service/s3/v1.88.3)

---
updated-dependencies:
- dependency-name: github.com/aws/aws-sdk-go-v2/service/s3
  dependency-version: 1.88.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-01 11:08:16 +08:00
dependabot[bot] e4267a4317
build(deps): bump github.com/helmfile/vals from 0.42.1 to 0.42.2 (#2200)
Bumps [github.com/helmfile/vals](https://github.com/helmfile/vals) from 0.42.1 to 0.42.2.
- [Release notes](https://github.com/helmfile/vals/releases)
- [Changelog](https://github.com/helmfile/vals/blob/main/.goreleaser.yml)
- [Commits](https://github.com/helmfile/vals/compare/v0.42.1...v0.42.2)

---
updated-dependencies:
- dependency-name: github.com/helmfile/vals
  dependency-version: 0.42.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-30 11:00:44 +08:00
Copilot d94a7ada2b
Migrate AWS SDK from v1 to v2 to resolve deprecation warnings (#2202)
* Migrate AWS SDK from v1 to v2 to resolve deprecation warnings

Co-authored-by: yxxhero <11087727+yxxhero@users.noreply.github.com>

* Fix error message style issue for staticcheck

Co-authored-by: yxxhero <11087727+yxxhero@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: yxxhero <11087727+yxxhero@users.noreply.github.com>
2025-09-30 09:59:00 +08:00
yxxhero c31ecf5061
cleanup disk in release ci (#2203)
* cleanup disk in release ci

Signed-off-by: yxxhero <aiopsclub@163.com>

* cleanup disk in release ci

Signed-off-by: yxxhero <aiopsclub@163.com>

* cleanup disk in release ci

Signed-off-by: yxxhero <aiopsclub@163.com>

* cleanup disk in release ci

Signed-off-by: yxxhero <aiopsclub@163.com>

---------

Signed-off-by: yxxhero <aiopsclub@163.com>
2025-09-30 09:01:42 +08:00
Davood a30957409d
fix typos in both comment and error message (#2199)
Signed-off-by: davood <falahati.davood@gmail.com>
2025-09-26 15:46:53 -04:00
dependabot[bot] c354768e60
build(deps): bump github.com/hashicorp/go-getter from 1.8.0 to 1.8.1 (#2194)
Bumps [github.com/hashicorp/go-getter](https://github.com/hashicorp/go-getter) from 1.8.0 to 1.8.1.
- [Release notes](https://github.com/hashicorp/go-getter/releases)
- [Changelog](https://github.com/hashicorp/go-getter/blob/main/.goreleaser.yml)
- [Commits](https://github.com/hashicorp/go-getter/compare/v1.8.0...v1.8.1)

---
updated-dependencies:
- dependency-name: github.com/hashicorp/go-getter
  dependency-version: 1.8.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-18 11:31:37 +08:00
dependabot[bot] 4de4c46f87
build(deps): bump github.com/helmfile/chartify from 0.24.7 to 0.25.0 (#2190)
Bumps [github.com/helmfile/chartify](https://github.com/helmfile/chartify) from 0.24.7 to 0.25.0.
- [Release notes](https://github.com/helmfile/chartify/releases)
- [Commits](https://github.com/helmfile/chartify/compare/v0.24.7...v0.25.0)

---
updated-dependencies:
- dependency-name: github.com/helmfile/chartify
  dependency-version: 0.25.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-13 17:19:16 +08:00
Ruslan d646b3cbd4
feat: Implement caching for pulling OCI charts (#2171)
Signed-off-by: Ruslan Khizhnyak <mustdiechik@gmail.com>
Co-authored-by: Ruslan Khizhnyak <rkhizhnyak@ptsecurity.com>
2025-09-13 12:49:03 +08:00
yxxhero 3f5d4110f6
build: update helm-diff plugin to v3.13.0 (#2189) 2025-09-13 10:08:15 +08:00
yxxhero c443baa103
build: update Helm to v3.19.0 across all components (#2187)
Signed-off-by: yxxhero <aiopsclub@163.com>
2025-09-12 13:58:08 -04:00
dependabot[bot] 9c1b393b35
build(deps): bump go.yaml.in/yaml/v2 from 2.4.2 to 2.4.3 (#2183)
Bumps [go.yaml.in/yaml/v2](https://github.com/yaml/go-yaml) from 2.4.2 to 2.4.3.
- [Release notes](https://github.com/yaml/go-yaml/releases)
- [Commits](https://github.com/yaml/go-yaml/compare/v2.4.2...v2.4.3)

---
updated-dependencies:
- dependency-name: go.yaml.in/yaml/v2
  dependency-version: 2.4.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-12 06:10:11 +08:00
Copilot 3728b6f647
Remove deprecated --wait-retries flag support to fix Helm compatibility error (#2179)
* Remove --wait-retries flag support and update documentation

Co-authored-by: yxxhero <11087727+yxxhero@users.noreply.github.com>

* Fix unused helm parameter in appendWaitFlags function

Co-authored-by: yxxhero <11087727+yxxhero@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: yxxhero <11087727+yxxhero@users.noreply.github.com>
2025-09-11 13:57:28 +08:00
dependabot[bot] 2ad21b3df0
build(deps): bump k8s.io/apimachinery from 0.34.0 to 0.34.1 (#2180)
Bumps [k8s.io/apimachinery](https://github.com/kubernetes/apimachinery) from 0.34.0 to 0.34.1.
- [Commits](https://github.com/kubernetes/apimachinery/compare/v0.34.0...v0.34.1)

---
updated-dependencies:
- dependency-name: k8s.io/apimachinery
  dependency-version: 0.34.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-11 12:32:23 +08:00
yxxhero e3de97fcbd
ci: update minikube and kubernetes versions (#2181)
* ci: update minikube and kubernetes versions

Signed-off-by: yxxhero <aiopsclub@163.com>

* debug minikube version

Signed-off-by: yxxhero <aiopsclub@163.com>

---------

Signed-off-by: yxxhero <aiopsclub@163.com>
2025-09-11 11:34:01 +08:00
dependabot[bot] 1ecffc87e4
build(deps): bump golang.org/x/sync from 0.16.0 to 0.17.0 (#2172)
Bumps [golang.org/x/sync](https://github.com/golang/sync) from 0.16.0 to 0.17.0.
- [Commits](https://github.com/golang/sync/compare/v0.16.0...v0.17.0)

---
updated-dependencies:
- dependency-name: golang.org/x/sync
  dependency-version: 0.17.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-09 07:16:11 +08:00
Nick Neisen f708d06200
Fix panic when helm isn't installed (#2169)
Return error instead of panic

Signed-off-by: Nick Neisen <nwneisen@gmail.com>
2025-09-09 07:15:46 +08:00
dependabot[bot] 55030e4eee
build(deps): bump github.com/zclconf/go-cty from 1.16.4 to 1.17.0 (#2173)
Bumps [github.com/zclconf/go-cty](https://github.com/zclconf/go-cty) from 1.16.4 to 1.17.0.
- [Release notes](https://github.com/zclconf/go-cty/releases)
- [Changelog](https://github.com/zclconf/go-cty/blob/main/CHANGELOG.md)
- [Commits](https://github.com/zclconf/go-cty/compare/v1.16.4...v1.17.0)

---
updated-dependencies:
- dependency-name: github.com/zclconf/go-cty
  dependency-version: 1.17.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-09 06:17:21 +08:00
dependabot[bot] 27d6fb08c6
build(deps): bump golang.org/x/term from 0.34.0 to 0.35.0 (#2174)
Bumps [golang.org/x/term](https://github.com/golang/term) from 0.34.0 to 0.35.0.
- [Commits](https://github.com/golang/term/compare/v0.34.0...v0.35.0)

---
updated-dependencies:
- dependency-name: golang.org/x/term
  dependency-version: 0.35.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-09 06:17:04 +08:00
dependabot[bot] 6fc2278f5f
build(deps): bump github.com/hashicorp/go-getter from 1.7.10 to 1.8.0 (#2175)
Bumps [github.com/hashicorp/go-getter](https://github.com/hashicorp/go-getter) from 1.7.10 to 1.8.0.
- [Release notes](https://github.com/hashicorp/go-getter/releases)
- [Changelog](https://github.com/hashicorp/go-getter/blob/main/.goreleaser.yml)
- [Commits](https://github.com/hashicorp/go-getter/commits/v1.8.0)

---
updated-dependencies:
- dependency-name: github.com/hashicorp/go-getter
  dependency-version: 1.8.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-09 06:16:41 +08:00
Nick Neisen 2116c93cc4
Add helm diff installation to README (#2170)
Add helm-diff to install section

Signed-off-by: Nick Neisen <nwneisen@gmail.com>
2025-09-06 13:28:26 +08:00
dependabot[bot] fc900dda54
build(deps): bump github.com/spf13/pflag from 1.0.9 to 1.0.10 (#2163) 2025-09-04 23:02:06 +00:00
dependabot[bot] 074de257f8
build(deps): bump github.com/hashicorp/go-getter from 1.7.9 to 1.7.10 (#2165)
Bumps [github.com/hashicorp/go-getter](https://github.com/hashicorp/go-getter) from 1.7.9 to 1.7.10.
- [Release notes](https://github.com/hashicorp/go-getter/releases)
- [Changelog](https://github.com/hashicorp/go-getter/blob/main/.goreleaser.yml)
- [Commits](https://github.com/hashicorp/go-getter/compare/v1.7.9...v1.7.10)

---
updated-dependencies:
- dependency-name: github.com/hashicorp/go-getter
  dependency-version: 1.7.10
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-05 06:32:17 +08:00
dependabot[bot] ce6197a514
build(deps): bump actions/setup-go from 5 to 6 (#2166)
Bumps [actions/setup-go](https://github.com/actions/setup-go) from 5 to 6.
- [Release notes](https://github.com/actions/setup-go/releases)
- [Commits](https://github.com/actions/setup-go/compare/v5...v6)

---
updated-dependencies:
- dependency-name: actions/setup-go
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-05 06:31:26 +08:00
Zubair Haque 31b3bd4e62
fix pflag error (#2164)
fix pflag error lint

Signed-off-by: zhaque44 <haque.zubair@gmail.com>
2025-09-05 06:30:53 +08:00
Copilot a9594eb158
Fix error propagation in helmfile diff when Kubernetes is unreachable (#2149)
* Fix error propagation in helmfile diff when Kubernetes is unreachable

Co-authored-by: yxxhero <11087727+yxxhero@users.noreply.github.com>

* Fix golangci-lint issue: replace custom contains function with strings.Contains

Co-authored-by: zhaque44 <20215376+zhaque44@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: yxxhero <11087727+yxxhero@users.noreply.github.com>
Co-authored-by: zhaque44 <20215376+zhaque44@users.noreply.github.com>
2025-09-02 12:51:17 -04:00
dependabot[bot] a5814ff01c
build(deps): bump github.com/spf13/cobra from 1.9.1 to 1.10.1 (#2162)
Bumps [github.com/spf13/cobra](https://github.com/spf13/cobra) from 1.9.1 to 1.10.1.
- [Release notes](https://github.com/spf13/cobra/releases)
- [Commits](https://github.com/spf13/cobra/compare/v1.9.1...v1.10.1)

---
updated-dependencies:
- dependency-name: github.com/spf13/cobra
  dependency-version: 1.10.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-02 17:48:41 +08:00
dependabot[bot] 7842a0cd09
build(deps): bump github.com/spf13/pflag from 1.0.7 to 1.0.9 (#2160)
Bumps [github.com/spf13/pflag](https://github.com/spf13/pflag) from 1.0.7 to 1.0.9.
- [Release notes](https://github.com/spf13/pflag/releases)
- [Commits](https://github.com/spf13/pflag/compare/v1.0.7...v1.0.9)

---
updated-dependencies:
- dependency-name: github.com/spf13/pflag
  dependency-version: 1.0.9
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-02 09:15:09 +08:00
dependabot[bot] ae9b6872db
build(deps): bump github.com/helmfile/vals from 0.42.0 to 0.42.1 (#2161)
Bumps [github.com/helmfile/vals](https://github.com/helmfile/vals) from 0.42.0 to 0.42.1.
- [Release notes](https://github.com/helmfile/vals/releases)
- [Changelog](https://github.com/helmfile/vals/blob/main/.goreleaser.yml)
- [Commits](https://github.com/helmfile/vals/compare/v0.42.0...v0.42.1)

---
updated-dependencies:
- dependency-name: github.com/helmfile/vals
  dependency-version: 0.42.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: yxxhero <11087727+yxxhero@users.noreply.github.com>
2025-09-02 08:25:49 +08:00
Copilot d14e894cf3
Bump github.com/ulikunitz/xz from v0.5.14 to v0.5.15 (#2159)
* Initial plan

* Bump github.com/ulikunitz/xz from v0.5.14 to v0.5.15

Co-authored-by: yxxhero <11087727+yxxhero@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: yxxhero <11087727+yxxhero@users.noreply.github.com>
2025-09-01 18:44:02 +08:00
dependabot[bot] 6d756bdf8a
build(deps): bump github.com/ulikunitz/xz from 0.5.10 to 0.5.14 (#2154)
Bumps [github.com/ulikunitz/xz](https://github.com/ulikunitz/xz) from 0.5.10 to 0.5.14.
- [Commits](https://github.com/ulikunitz/xz/compare/v0.5.10...v0.5.14)

---
updated-dependencies:
- dependency-name: github.com/ulikunitz/xz
  dependency-version: 0.5.14
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-29 06:57:59 +08:00
dependabot[bot] 0ac9ea7993
build(deps): bump github.com/stretchr/testify from 1.11.0 to 1.11.1 (#2151)
Bumps [github.com/stretchr/testify](https://github.com/stretchr/testify) from 1.11.0 to 1.11.1.
- [Release notes](https://github.com/stretchr/testify/releases)
- [Commits](https://github.com/stretchr/testify/compare/v1.11.0...v1.11.1)

---
updated-dependencies:
- dependency-name: github.com/stretchr/testify
  dependency-version: 1.11.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-28 06:54:55 +08:00
Copilot d37f937c9e
Fix enableDNS flag missing in diff command and refactor duplicate logic (#2147)
* Initial plan

* Add enableDNS flag support to diff command

Co-authored-by: yxxhero <11087727+yxxhero@users.noreply.github.com>

* Extract EnableDNS flag logic into reusable function

Co-authored-by: yxxhero <11087727+yxxhero@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: yxxhero <11087727+yxxhero@users.noreply.github.com>
2025-08-27 14:32:17 +08:00
Copilot 135ff63aa3
Add missing --timeout flag to helmfile sync command with documentation (#2148)
* Initial plan

* Implement --timeout flag for helmfile sync command

- Add Timeout field to SyncOptions struct in pkg/config/sync.go
- Add --timeout flag to sync command in cmd/sync.go
- Add Timeout field to SyncOpts struct in pkg/state/state.go
- Modify timeoutFlags() function to prioritize CLI timeout over release and default configs
- Add test case to verify CLI timeout overrides other timeout settings
- Follow same pattern as existing --wait and --wait-for-jobs flags

Co-authored-by: yxxhero <11087727+yxxhero@users.noreply.github.com>

* Fix lint issues: format test struct fields properly

Co-authored-by: yxxhero <11087727+yxxhero@users.noreply.github.com>

* Update docs: Add --timeout flag documentation for helmfile sync command

Co-authored-by: yxxhero <11087727+yxxhero@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: yxxhero <11087727+yxxhero@users.noreply.github.com>
2025-08-27 14:32:02 +08:00
dependabot[bot] e695637b08
build(deps): bump github.com/stretchr/testify from 1.10.0 to 1.11.0 (#2150)
Bumps [github.com/stretchr/testify](https://github.com/stretchr/testify) from 1.10.0 to 1.11.0.
- [Release notes](https://github.com/stretchr/testify/releases)
- [Commits](https://github.com/stretchr/testify/compare/v1.10.0...v1.11.0)

---
updated-dependencies:
- dependency-name: github.com/stretchr/testify
  dependency-version: 1.11.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-26 20:42:06 +08:00
yxxhero a05b93de5c
build: update helm to v3.18.6 (#2144)
Signed-off-by: yxxhero <aiopsclub@163.com>
2025-08-22 14:10:01 +08:00
dependabot[bot] 008a5322bd
build(deps): bump github.com/zclconf/go-cty from 1.16.3 to 1.16.4 (#2145)
Bumps [github.com/zclconf/go-cty](https://github.com/zclconf/go-cty) from 1.16.3 to 1.16.4.
- [Release notes](https://github.com/zclconf/go-cty/releases)
- [Changelog](https://github.com/zclconf/go-cty/blob/main/CHANGELOG.md)
- [Commits](https://github.com/zclconf/go-cty/compare/v1.16.3...v1.16.4)

---
updated-dependencies:
- dependency-name: github.com/zclconf/go-cty
  dependency-version: 1.16.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-22 13:53:22 +08:00
dependabot[bot] 0fa965e011
build(deps): bump github.com/hashicorp/go-getter from 1.7.8 to 1.7.9 (#2139)
Bumps [github.com/hashicorp/go-getter](https://github.com/hashicorp/go-getter) from 1.7.8 to 1.7.9.
- [Release notes](https://github.com/hashicorp/go-getter/releases)
- [Changelog](https://github.com/hashicorp/go-getter/blob/main/.goreleaser.yml)
- [Commits](https://github.com/hashicorp/go-getter/compare/v1.7.8...v1.7.9)

---
updated-dependencies:
- dependency-name: github.com/hashicorp/go-getter
  dependency-version: 1.7.9
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-19 07:08:09 +08:00
dependabot[bot] 33b6fca12c
build(deps): bump github.com/helmfile/chartify from 0.24.6 to 0.24.7 (#2135)
Bumps [github.com/helmfile/chartify](https://github.com/helmfile/chartify) from 0.24.6 to 0.24.7.
- [Release notes](https://github.com/helmfile/chartify/releases)
- [Commits](https://github.com/helmfile/chartify/compare/v0.24.6...v0.24.7)

---
updated-dependencies:
- dependency-name: github.com/helmfile/chartify
  dependency-version: 0.24.7
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-15 07:08:12 +08:00
dependabot[bot] ceef3f1a6b
build(deps): bump k8s.io/apimachinery from 0.33.3 to 0.33.4 (#2136)
Bumps [k8s.io/apimachinery](https://github.com/kubernetes/apimachinery) from 0.33.3 to 0.33.4.
- [Commits](https://github.com/kubernetes/apimachinery/compare/v0.33.3...v0.33.4)

---
updated-dependencies:
- dependency-name: k8s.io/apimachinery
  dependency-version: 0.33.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-15 07:07:50 +08:00
Copilot 7f18858182
Fix parseHelmVersion to handle helm versions without 'v' prefix (#2132)
* Initial plan

* Fix panic in helmfile init when parsing invalid helm versions

Co-authored-by: yxxhero <11087727+yxxhero@users.noreply.github.com>

* Fix parseHelmVersion to handle versions without v prefix

Co-authored-by: yxxhero <11087727+yxxhero@users.noreply.github.com>

* Simplify parseHelmVersion function to be more readable

Co-authored-by: yxxhero <11087727+yxxhero@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: yxxhero <11087727+yxxhero@users.noreply.github.com>
2025-08-14 21:11:09 +08:00
yxxhero 8c123dcdda
refactor(state): extract getMissingFileHandler method for clarity (#2133)
* refactor(state): extract getMissingFileHandler method for clarity

Signed-off-by: yxxhero <aiopsclub@163.com>
2025-08-14 21:10:44 +08:00
Copilot bb6df72463
Add comprehensive .github/copilot-instructions.md for coding agents (#2131) 2025-08-14 10:21:26 +08:00
yxxhero 444275281f
Update recommended Helm versions in init.go and run.sh (#2129)
- Bump HelmDiffRecommendedVersion from v3.12.3 to v3.12.5 in pkg/app/init.go
- Bump default HELM_DIFF_VERSION from 3.12.3 to 3.12.5 in test/integration/run.sh
- Update HelmRecommendedVersion from v3.18.4 to v3.18.5 in pkg/app/init.go

Signed-off-by: yxxhero <aiopsclub@163.com>
2025-08-14 08:41:43 +08:00
dependabot[bot] 4aae348a46
build(deps): bump actions/checkout from 4 to 5 (#2128)
Bumps [actions/checkout](https://github.com/actions/checkout) from 4 to 5.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-12 07:44:59 +08:00
dependabot[bot] 9dbb4a4a27
build(deps): bump golang.org/x/term from 0.33.0 to 0.34.0 (#2123)
Bumps [golang.org/x/term](https://github.com/golang/term) from 0.33.0 to 0.34.0.
- [Commits](https://github.com/golang/term/compare/v0.33.0...v0.34.0)

---
updated-dependencies:
- dependency-name: golang.org/x/term
  dependency-version: 0.34.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-08 21:27:42 +08:00
dependabot[bot] 64d676a7e3
build(deps): bump actions/download-artifact from 4 to 5 (#2121)
Bumps [actions/download-artifact](https://github.com/actions/download-artifact) from 4 to 5.
- [Release notes](https://github.com/actions/download-artifact/releases)
- [Commits](https://github.com/actions/download-artifact/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/download-artifact
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-07 06:28:40 +08:00
yxxhero 959aae5791
refactor(yaml): switch yaml library import paths from gopkg.in to go.yaml.in (#2114)
Signed-off-by: yxxhero <aiopsclub@163.com>
2025-08-01 11:28:39 +08:00
Jess 9a88372449
Allow caching of remote files to be disabled (#2112)
* Allow caching of remote files to be disabled

Make it possible to automatically update the cache of remote
resources by disabling the caching of those resources using a query
string parameter (`cache=false`).

Signed-off-by: Jess <jess@ros.io>

* Fix test that broke

Because query parameters are being re-encoded, = is being encoded to %3D.

Signed-off-by: Jess <jess@ros.io>

* Add test for disabling caching of remote resources

Signed-off-by: Jess <jess@ros.io>

* Include example usage in docs

Signed-off-by: Jess <jess@ros.io>

---------

Signed-off-by: Jess <jess@ros.io>
2025-07-31 13:38:36 +08:00
yxxhero a76bec234c
refactor(filesystem): add CopyDir method and optimize Fetch function (#2111)
* refactor(filesystem): add CopyDir method and optimize Fetch function

Signed-off-by: yxxhero <aiopsclub@163.com>

* fix(state): conditionally prepare charts for local helmfile command

Signed-off-by: yxxhero <aiopsclub@163.com>

* fix(state): conditionally prepare charts for local helmfile command

Signed-off-by: yxxhero <aiopsclub@163.com>

* refactor(state): optimize chart path generation and update dependencies

Signed-off-by: yxxhero <aiopsclub@163.com>

* fix(test): update path in fetch-forl-local-chart test

Signed-off-by: yxxhero <aiopsclub@163.com>

* add more test cases

Signed-off-by: yxxhero <aiopsclub@163.com>

---------

Signed-off-by: yxxhero <aiopsclub@163.com>
2025-07-28 16:10:25 -04:00
yxxhero b0911ab1a2
feat(state): add missingFileHandlerConfig and related logic (#2105)
* feat(state): add missingFileHandlerConfig and related logic

Signed-off-by: yxxhero <aiopsclub@163.com>

* feat(state): add missingFileHandlerConfig and related logic

Signed-off-by: yxxhero <aiopsclub@163.com>

---------

Signed-off-by: yxxhero <aiopsclub@163.com>
2025-07-21 19:15:51 -04:00
dependabot[bot] 6fd4048653
build(deps): bump github.com/spf13/pflag from 1.0.6 to 1.0.7 (#2104)
Bumps [github.com/spf13/pflag](https://github.com/spf13/pflag) from 1.0.6 to 1.0.7.
- [Release notes](https://github.com/spf13/pflag/releases)
- [Commits](https://github.com/spf13/pflag/compare/v1.0.6...v1.0.7)

---
updated-dependencies:
- dependency-name: github.com/spf13/pflag
  dependency-version: 1.0.7
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-17 19:05:50 -04:00
yxxhero 4a3f923b1a
fix: update Helm version to v3.17.4 in CI and init.go (#2102)
Signed-off-by: yxxhero <aiopsclub@163.com>
2025-07-17 19:01:31 -04:00
dependabot[bot] 899b7791d2
build(deps): bump k8s.io/apimachinery from 0.33.2 to 0.33.3 (#2101)
Bumps [k8s.io/apimachinery](https://github.com/kubernetes/apimachinery) from 0.33.2 to 0.33.3.
- [Commits](https://github.com/kubernetes/apimachinery/compare/v0.33.2...v0.33.3)

---
updated-dependencies:
- dependency-name: k8s.io/apimachinery
  dependency-version: 0.33.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-17 06:35:01 +08:00
dependabot[bot] 0b29a3bf31
build(deps): bump github.com/helmfile/vals from 0.41.2 to 0.41.3 (#2100)
Bumps [github.com/helmfile/vals](https://github.com/helmfile/vals) from 0.41.2 to 0.41.3.
- [Release notes](https://github.com/helmfile/vals/releases)
- [Changelog](https://github.com/helmfile/vals/blob/main/.goreleaser.yml)
- [Commits](https://github.com/helmfile/vals/compare/v0.41.2...v0.41.3)

---
updated-dependencies:
- dependency-name: github.com/helmfile/vals
  dependency-version: 0.41.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-15 06:03:58 +08:00
60 changed files with 2976 additions and 2515 deletions

234
.github/copilot-instructions.md vendored Normal file
View File

@ -0,0 +1,234 @@
# Copilot Instructions for Helmfile
## Repository Overview
Helmfile is a tool for deploying Helm charts that manages Kubernetes deployments as code. It provides templating, environment management, and GitOps workflows for Helm chart deployments.
Helmfile is a declarative tool. In Helmfile, all elements of the desired state for deployments must be included in the `helmfile.yaml` config file and any associated files. Only operational matters can be provided dynamically, via command-line flags and environment variables.
**Key Details:**
- **Language:** Go 1.24.2+
- **Type:** CLI tool / Kubernetes deployment management
- **Size:** Large codebase (~229MB binary, 200+ dependencies)
- **Runtime:** Requires Helm 3.x as external dependency
- **Target Platform:** Linux/macOS/Windows, Kubernetes clusters
## Build and Validation Commands
### Essential Setup
Helmfile requires Helm 3.x and Kustomize 5.x as runtime dependencies:
```bash
# Check for Helm dependency (REQUIRED)
helm version # Must show version 3.x
# Initialize Helm plugins after helmfile installation
./helmfile init # Installs required helm-diff plugin
# Alternative: Force install without prompts
./helmfile init --force
```
### Build Process
```bash
# Standard build (takes 2-3 minutes due to many dependencies)
make build
# Alternative direct build
go build -o helmfile .
# Build with test tools (required for integration tests, ~1 minute)
make build-test-tools # Creates diff-yamls and downloads dyff
# Cross-platform builds
make cross
```
**Build Timing:** First build downloads 200+ Go packages and takes 2-3 minutes. Subsequent builds are faster due to module cache. Test tools build is faster (~1 minute).
### Validation Pipeline
Run in this exact order to match CI requirements:
```bash
# 1. Code formatting and linting
make check # Run go vet (required - always works)
# Note: make fmt requires gci tool (go install github.com/daixiang0/gci@latest)
# 2. Unit tests (fast, ~30 seconds)
go test -v ./pkg/... -race -p=1
# 3. Integration tests (requires Kubernetes - see Environment Setup)
make integration # Takes 5-10 minutes, needs minikube/k8s cluster
# 4. E2E tests (optional, needs expect package)
sudo apt-get install expect # On Ubuntu/Debian
bash test/e2e/helmfile-init/init_linux.sh
```
### Linting Configuration
Uses golangci-lint with configuration in `.golangci.yaml`. Install via:
```bash
# For local development
curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v2.1.6
golangci-lint run
```
**Critical Lint Rules:** staticcheck, errcheck, revive, unused. Fix lint errors before committing.
## Environment Setup Requirements
### Dependencies for Development
```bash
# Required for building/testing
go version # Must be 1.24.2+
helm version # Must be 3.x
kubectl version # For K8s integration
# Required for integration tests
minikube start # Or other K8s cluster
kustomize version # v5.2.1+ for some tests
```
### Integration Test Environment
Integration tests require a running Kubernetes cluster:
```bash
# Using minikube (recommended for CI)
minikube start
export KUBECONFIG=$(minikube kubeconfig-path)
# Using kind (alternative)
kind create cluster --name helmfile-test
# Verify cluster access
kubectl cluster-info
```
**Timing:** Integration tests take 5-10 minutes and may fail due to timing issues in resource-constrained environments.
## Project Architecture and Layout
### Core Directory Structure
```
/
├── main.go # Entry point - CLI initialization and signal handling
├── cmd/ # CLI commands (apply, diff, sync, template, etc.)
│ ├── root.go # Main cobra command setup and global flags
│ ├── apply.go # helmfile apply command
│ ├── diff.go # helmfile diff command
│ └── ... # Other subcommands
├── pkg/ # Core library packages
│ ├── app/ # Main application logic and execution
│ ├── state/ # Helmfile state management and chart dependencies
│ ├── helmexec/ # Helm execution and command wrapper
│ ├── tmpl/ # Go template processing and functions
│ ├── environment/ # Environment and values management
│ └── ... # Other core packages
├── test/ # Test suites
│ ├── integration/ # Integration tests with real K8s clusters
│ ├── e2e/ # End-to-end user workflow tests
│ └── advanced/ # Advanced feature tests
├── docs/ # Documentation (mkdocs format)
├── examples/ # Example helmfile configurations
└── .github/workflows/ # CI/CD pipeline definitions
```
### Key Source Files
- **main.go:** Signal handling, CLI execution entry point
- **cmd/root.go:** Global CLI configuration, error handling, logging setup
- **pkg/app/app.go:** Main application orchestration, state management
- **pkg/state/state.go:** Helmfile state parsing, release management
- **pkg/helmexec/exec.go:** Helm command execution, version detection
### Configuration Files
- **go.mod/go.sum:** Go dependencies (many cloud providers, k8s libraries)
- **.golangci.yaml:** Linting rules and settings
- **Makefile:** Build targets and development workflows
- **mkdocs.yml:** Documentation generation configuration
- **.github/workflows/ci.yaml:** Complete CI pipeline definition
## Continuous Integration Pipeline
### GitHub Actions Workflow (`.github/workflows/ci.yaml`)
1. **Lint Job:** golangci-lint with custom configuration (~5 minutes)
2. **Test Job:** Unit tests + binary build (~10 minutes)
3. **Integration Job:** Tests with multiple Helm/Kustomize versions (~15-20 minutes each)
4. **E2E Job:** User workflow validation (~5 minutes)
**Matrix Testing:** CI tests against multiple Helm versions (3.17.x, 3.18.x) and Kustomize versions (5.2.x, 5.4.x).
### Pre-commit Validation Steps
Always run these locally before pushing:
```bash
make check # Format and vet (required)
go test ./pkg/... # Unit tests
make build # Verify build works
# Note: make fmt requires gci tool: go install github.com/daixiang0/gci@latest
```
### Common CI Failure Causes
- **Linting errors:** Run `golangci-lint run` locally first
- **Integration test timeouts:** K8s cluster setup timing issues
- **Version compatibility:** Ensure Go 1.24.2+ and Helm 3.x
- **Race conditions:** Some tests are sensitive to parallel execution
## Development Gotchas and Known Issues
### Build Issues
- **Long initial build time:** First `make build` downloads 200+ packages (~2-3 minutes)
- **Memory usage:** Large binary size due to embedded dependencies
- **Git tags:** Build may show version warnings if not on tagged commit
- **Tool dependencies:** `make fmt` requires `gci` tool installation
### Testing Issues
- **Integration tests require K8s:** Will fail without cluster access
- **Test isolation:** Use `-p=1` flag to avoid race conditions
- **Minikube timing:** May need to wait for cluster ready state
- **Plugin dependencies:** Tests need helm-diff and helm-secrets plugins
### Runtime Requirements
- **Helm dependency:** Always required at runtime, not just build time (available in CI)
- **kubectl access:** Most operations need valid kubeconfig
- **Plugin management:** `helmfile init` must be run after installation
### Common Error Patterns
```bash
# Missing Helm
"helm: command not found" → Install Helm first
# Plugin missing
"Error: plugin 'diff' not found" → Run helmfile init
# K8s access
"connection refused" → Check kubectl cluster-info
# Permission errors
"permission denied" → Check kubeconfig and cluster access
# Missing tools
"gci: No such file or directory" → go install github.com/daixiang0/gci@latest
```
## Working with the Codebase
### Making Changes
- **Follow Helmfile design**: Helmfile is a declarative deployment tool. Anything that is part of the desired state of the deployments needs to be managed by Helmfile configs. Only operational knowledge that affects "how" to apply the desired state needs to be runtime options, like command-like flags and environment variables.
- **Small, focused changes:** Each PR should address single concern
- **Test coverage:** Add unit tests for new pkg/ functionality
- **Integration tests:** Update test-cases/ for new CLI features
- **Documentation:** Update docs/ for user-facing changes
### Key Packages to Understand
- **pkg/app:** Main business logic, start here for feature changes
- **pkg/state:** Helmfile parsing and release orchestration
- **cmd/:** CLI interface changes and new subcommands
- **pkg/helmexec:** Helm integration and command execution
### Architecture Patterns
- **Dependency injection:** App uses interfaces for testability
- **State management:** Immutable state objects, functional transforms
- **Error handling:** Custom error types with exit codes
- **Plugin system:** Extensible via Helm plugins and Go templates
---
**Trust these instructions:** This information is validated against the current codebase. Only search for additional details if these instructions are incomplete or found to be incorrect for your specific task.

View File

@ -13,8 +13,8 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
- uses: actions/checkout@v5
- uses: actions/setup-go@v6
with:
go-version-file: go.mod
cache: false
@ -25,10 +25,10 @@ jobs:
tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
with:
fetch-depth: 0
- uses: actions/setup-go@v5
- uses: actions/setup-go@v6
with:
go-version-file: go.mod
- name: Build
@ -37,7 +37,7 @@ jobs:
run: make check test
- name: Archive built binaries
run: tar -cvf built-binaries.tar helmfile diff-yamls dyff
- uses: actions/upload-artifact@v4
- uses: actions/upload-artifact@v5
with:
name: built-binaries-${{ github.run_id }}
path: built-binaries.tar
@ -54,12 +54,12 @@ jobs:
# Helm maintains the latest minor version only and therefore each Helmfile version supports 2 Helm minor versions.
# That's why we cover only 2 Helm minor versions in this matrix.
# See https://github.com/helmfile/helmfile/pull/286#issuecomment-1250161182 for more context.
- helm-version: v3.17.3
- helm-version: v3.18.6
kustomize-version: v5.2.1
plugin-secrets-version: 4.6.5
plugin-diff-version: 3.11.0
extra-helmfile-flags: ''
- helm-version: v3.17.3
- helm-version: v3.18.6
kustomize-version: v5.4.3
# We assume that the helm-secrets plugin is supposed to
# work with the two most recent helm minor versions.
@ -67,32 +67,32 @@ jobs:
# we will mark this combination as failable,
# and instruct users to upgrade helm and helm-secrets at once.
plugin-secrets-version: 4.6.5
plugin-diff-version: 3.12.3
plugin-diff-version: 3.12.5
extra-helmfile-flags: ''
- helm-version: v3.18.4
- helm-version: v3.19.0
kustomize-version: v5.2.1
plugin-secrets-version: 4.6.5
plugin-diff-version: 3.11.0
extra-helmfile-flags: ''
- helm-version: v3.18.4
- helm-version: v3.19.0
kustomize-version: v5.4.3
plugin-secrets-version: 4.6.5
plugin-diff-version: 3.12.3
plugin-diff-version: 3.12.5
extra-helmfile-flags: ''
# In case you need to test some optional helmfile features,
# enable it via extra-helmfile-flags below.
- helm-version: v3.18.4
- helm-version: v3.19.0
kustomize-version: v5.4.3
plugin-secrets-version: 4.6.5
plugin-diff-version: 3.12.3
plugin-diff-version: 3.12.5
extra-helmfile-flags: '--enable-live-output'
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
- uses: actions/checkout@v5
- uses: actions/setup-go@v6
with:
go-version-file: go.mod
- uses: actions/download-artifact@v4
- uses: actions/download-artifact@v6
with:
name: built-binaries-${{ github.run_id }}
- name: install semver
@ -109,6 +109,8 @@ jobs:
run: make -C .github/workflows helm vault sops kustomize
- name: Start minikube
uses: medyagh/setup-minikube@latest
with:
kubernetes-version: v1.33.1
- name: Execute integration tests
run: make integration
env:
@ -122,10 +124,10 @@ jobs:
needs: tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
with:
fetch-depth: 0
- uses: actions/download-artifact@v4
- uses: actions/download-artifact@v6
with:
name: built-binaries-${{ github.run_id }}
- name: Extract tar to get built binaries

View File

@ -39,7 +39,7 @@ jobs:
suffix: "-ubuntu"
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v5
with:
fetch-depth: 0

View File

@ -22,12 +22,25 @@ jobs:
goreleaser:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
with:
fetch-depth: 0
- uses: actions/setup-go@v5
- uses: actions/setup-go@v6
with:
go-version-file: go.mod
- name: check disk usage
run: df -h
- name: cleanup disk
run: |
sudo rm -rf /usr/share/dotnet
sudo rm -rf /opt/ghc
sudo rm -rf /usr/local/share/boost
sudo rm -fr /usr/local/lib/android
sudo rm -fr /opt/hostedtoolcache/CodeQL
sudo docker image prune --all --force
sudo docker builder prune -a
- name: check disk usage
run: df -h
- uses: goreleaser/goreleaser-action@v6
with:
version: latest

View File

@ -45,7 +45,7 @@ linters:
lines: 280
statements: 140
gocognit:
min-complexity: 100
min-complexity: 110
goconst:
min-len: 3
min-occurrences: 8

View File

@ -12,11 +12,11 @@ RUN make static-${TARGETOS}-${TARGETARCH}
# -----------------------------------------------------------------------------
FROM alpine:3.19
FROM alpine:3.22
LABEL org.opencontainers.image.source=https://github.com/helmfile/helmfile
RUN apk add --no-cache ca-certificates git bash curl jq openssh-client gnupg
RUN apk add --no-cache ca-certificates git bash curl jq yq openssh-client gnupg
ARG TARGETARCH TARGETOS TARGETPLATFORM
@ -30,7 +30,7 @@ ENV HELM_CONFIG_HOME="${HELM_CONFIG_HOME}"
ARG HELM_DATA_HOME="${HOME}/.local/share/helm"
ENV HELM_DATA_HOME="${HELM_DATA_HOME}"
ARG HELM_VERSION="v3.18.4"
ARG HELM_VERSION="v3.19.0"
ENV HELM_VERSION="${HELM_VERSION}"
ARG HELM_LOCATION="https://get.helm.sh"
ARG HELM_FILENAME="helm-${HELM_VERSION}-${TARGETOS}-${TARGETARCH}.tar.gz"
@ -38,8 +38,8 @@ RUN set -x && \
curl --retry 5 --retry-connrefused -LO "${HELM_LOCATION}/${HELM_FILENAME}" && \
echo Verifying ${HELM_FILENAME}... && \
case ${TARGETPLATFORM} in \
"linux/amd64") HELM_SHA256="f8180838c23d7c7d797b208861fecb591d9ce1690d8704ed1e4cb8e2add966c1" ;; \
"linux/arm64") HELM_SHA256="c0a45e67eef0c7416a8a8c9e9d5d2d30d70e4f4d3f7bea5de28241fffa8f3b89" ;; \
"linux/amd64") HELM_SHA256="a7f81ce08007091b86d8bd696eb4d86b8d0f2e1b9f6c714be62f82f96a594496" ;; \
"linux/arm64") HELM_SHA256="440cf7add0aee27ebc93fada965523c1dc2e0ab340d4348da2215737fc0d76ad" ;; \
esac && \
echo "${HELM_SHA256} ${HELM_FILENAME}" | sha256sum -c && \
echo Extracting ${HELM_FILENAME}... && \
@ -93,7 +93,7 @@ RUN set -x && \
[ "$(age --version)" = "${AGE_VERSION}" ] && \
[ "$(age-keygen --version)" = "${AGE_VERSION}" ]
RUN helm plugin install https://github.com/databus23/helm-diff --version v3.12.3 && \
RUN helm plugin install https://github.com/databus23/helm-diff --version v3.13.1 && \
helm plugin install https://github.com/jkroepke/helm-secrets --version v4.6.5 && \
helm plugin install https://github.com/hypnoglow/helm-s3.git --version v0.16.3 && \
helm plugin install https://github.com/aslafy-z/helm-git.git --version v1.3.0 && \

View File

@ -25,6 +25,9 @@ RUN apt update -qq && \
ARG TARGETARCH TARGETOS TARGETPLATFORM
RUN wget https://github.com/mikefarah/yq/releases/latest/download/yq_${TARGETOS}_${TARGETARCH} -O /usr/local/bin/yq &&\
chmod +x /usr/local/bin/yq
# Set Helm home variables so that also non-root users can use plugins etc.
ARG HOME="/helm"
ENV HOME="${HOME}"
@ -35,7 +38,7 @@ ENV HELM_CONFIG_HOME="${HELM_CONFIG_HOME}"
ARG HELM_DATA_HOME="${HOME}/.local/share/helm"
ENV HELM_DATA_HOME="${HELM_DATA_HOME}"
ARG HELM_VERSION="v3.18.4"
ARG HELM_VERSION="v3.19.0"
ENV HELM_VERSION="${HELM_VERSION}"
ARG HELM_LOCATION="https://get.helm.sh"
ARG HELM_FILENAME="helm-${HELM_VERSION}-${TARGETOS}-${TARGETARCH}.tar.gz"
@ -43,8 +46,8 @@ RUN set -x && \
curl --retry 5 --retry-connrefused -LO "${HELM_LOCATION}/${HELM_FILENAME}" && \
echo Verifying ${HELM_FILENAME}... && \
case ${TARGETPLATFORM} in \
"linux/amd64") HELM_SHA256="f8180838c23d7c7d797b208861fecb591d9ce1690d8704ed1e4cb8e2add966c1" ;; \
"linux/arm64") HELM_SHA256="c0a45e67eef0c7416a8a8c9e9d5d2d30d70e4f4d3f7bea5de28241fffa8f3b89" ;; \
"linux/amd64") HELM_SHA256="a7f81ce08007091b86d8bd696eb4d86b8d0f2e1b9f6c714be62f82f96a594496" ;; \
"linux/arm64") HELM_SHA256="440cf7add0aee27ebc93fada965523c1dc2e0ab340d4348da2215737fc0d76ad" ;; \
esac && \
echo "${HELM_SHA256} ${HELM_FILENAME}" | sha256sum -c && \
echo Extracting ${HELM_FILENAME}... && \
@ -99,7 +102,7 @@ RUN set -x && \
[ "$(age --version)" = "${AGE_VERSION}" ] && \
[ "$(age-keygen --version)" = "${AGE_VERSION}" ]
RUN helm plugin install https://github.com/databus23/helm-diff --version v3.12.3 && \
RUN helm plugin install https://github.com/databus23/helm-diff --version v3.13.1 && \
helm plugin install https://github.com/jkroepke/helm-secrets --version v4.6.5 && \
helm plugin install https://github.com/hypnoglow/helm-s3.git --version v0.16.3 && \
helm plugin install https://github.com/aslafy-z/helm-git.git --version v1.3.0 && \

View File

@ -12,7 +12,7 @@ RUN make static-${TARGETOS}-${TARGETARCH}
# -----------------------------------------------------------------------------
FROM ubuntu:24.10
FROM ubuntu:24.04
LABEL org.opencontainers.image.source=https://github.com/helmfile/helmfile
@ -25,6 +25,9 @@ RUN apt update -qq && \
ARG TARGETARCH TARGETOS TARGETPLATFORM
RUN wget https://github.com/mikefarah/yq/releases/latest/download/yq_${TARGETOS}_${TARGETARCH} -O /usr/local/bin/yq &&\
chmod +x /usr/local/bin/yq
# Set Helm home variables so that also non-root users can use plugins etc.
ARG HOME="/helm"
ENV HOME="${HOME}"
@ -35,7 +38,7 @@ ENV HELM_CONFIG_HOME="${HELM_CONFIG_HOME}"
ARG HELM_DATA_HOME="${HOME}/.local/share/helm"
ENV HELM_DATA_HOME="${HELM_DATA_HOME}"
ARG HELM_VERSION="v3.18.4"
ARG HELM_VERSION="v3.19.0"
ENV HELM_VERSION="${HELM_VERSION}"
ARG HELM_LOCATION="https://get.helm.sh"
ARG HELM_FILENAME="helm-${HELM_VERSION}-${TARGETOS}-${TARGETARCH}.tar.gz"
@ -43,8 +46,8 @@ RUN set -x && \
curl --retry 5 --retry-connrefused -LO "${HELM_LOCATION}/${HELM_FILENAME}" && \
echo Verifying ${HELM_FILENAME}... && \
case ${TARGETPLATFORM} in \
"linux/amd64") HELM_SHA256="f8180838c23d7c7d797b208861fecb591d9ce1690d8704ed1e4cb8e2add966c1" ;; \
"linux/arm64") HELM_SHA256="c0a45e67eef0c7416a8a8c9e9d5d2d30d70e4f4d3f7bea5de28241fffa8f3b89" ;; \
"linux/amd64") HELM_SHA256="a7f81ce08007091b86d8bd696eb4d86b8d0f2e1b9f6c714be62f82f96a594496" ;; \
"linux/arm64") HELM_SHA256="440cf7add0aee27ebc93fada965523c1dc2e0ab340d4348da2215737fc0d76ad" ;; \
esac && \
echo "${HELM_SHA256} ${HELM_FILENAME}" | sha256sum -c && \
echo Extracting ${HELM_FILENAME}... && \
@ -99,7 +102,7 @@ RUN set -x && \
[ "$(age --version)" = "${AGE_VERSION}" ] && \
[ "$(age-keygen --version)" = "${AGE_VERSION}" ]
RUN helm plugin install https://github.com/databus23/helm-diff --version v3.12.3 && \
RUN helm plugin install https://github.com/databus23/helm-diff --version v3.13.1 && \
helm plugin install https://github.com/jkroepke/helm-secrets --version v4.6.5 && \
helm plugin install https://github.com/hypnoglow/helm-s3.git --version v0.16.3 && \
helm plugin install https://github.com/aslafy-z/helm-git.git --version v1.3.0 && \

View File

@ -17,6 +17,7 @@
[![Slack Community #helmfile](https://slack.sweetops.com/badge.svg)](https://slack.sweetops.com)
[![Documentation](https://readthedocs.org/projects/helmfile/badge/?version=latest&style=flat)](https://helmfile.readthedocs.io/en/latest/)
[![Gurubase](https://img.shields.io/badge/Gurubase-Ask%20Helmfile%20Guru-006BFF)](https://gurubase.io/g/helmfile)
[![zread](https://img.shields.io/badge/Ask_Zread-_.svg?style=flat&color=00b0aa&labelColor=000000&logo=data%3Aimage%2Fsvg%2Bxml%3Bbase64%2CPHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIHZpZXdCb3g9IjAgMCAxNiAxNiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTQuOTYxNTYgMS42MDAxSDIuMjQxNTZDMS44ODgxIDEuNjAwMSAxLjYwMTU2IDEuODg2NjQgMS42MDE1NiAyLjI0MDFWNC45NjAxQzEuNjAxNTYgNS4zMTM1NiAxLjg4ODEgNS42MDAxIDIuMjQxNTYgNS42MDAxSDQuOTYxNTZDNS4zMTUwMiA1LjYwMDEgNS42MDE1NiA1LjMxMzU2IDUuNjAxNTYgNC45NjAxVjIuMjQwMUM1LjYwMTU2IDEuODg2NjQgNS4zMTUwMiAxLjYwMDEgNC45NjE1NiAxLjYwMDFaIiBmaWxsPSIjZmZmIi8%2BCjxwYXRoIGQ9Ik00Ljk2MTU2IDEwLjM5OTlIMi4yNDE1NkMxLjg4ODEgMTAuMzk5OSAxLjYwMTU2IDEwLjY4NjQgMS42MDE1NiAxMS4wMzk5VjEzLjc1OTlDMS42MDE1NiAxNC4xMTM0IDEuODg4MSAxNC4zOTk5IDIuMjQxNTYgMTQuMzk5OUg0Ljk2MTU2QzUuMzE1MDIgMTQuMzk5OSA1LjYwMTU2IDE0LjExMzQgNS42MDE1NiAxMy43NTk5VjExLjAzOTlDNS42MDE1NiAxMC42ODY0IDUuMzE1MDIgMTAuMzk5OSA0Ljk2MTU2IDEwLjM5OTlaIiBmaWxsPSIjZmZmIi8%2BCjxwYXRoIGQ9Ik0xMy43NTg0IDEuNjAwMUgxMS4wMzg0QzEwLjY4NSAxLjYwMDEgMTAuMzk4NCAxLjg4NjY0IDEwLjM5ODQgMi4yNDAxVjQuOTYwMUMxMC4zOTg0IDUuMzEzNTYgMTAuNjg1IDUuNjAwMSAxMS4wMzg0IDUuNjAwMUgxMy43NTg0QzE0LjExMTkgNS42MDAxIDE0LjM5ODQgNS4zMTM1NiAxNC4zOTg0IDQuOTYwMVYyLjI0MDFDMTQuMzk4NCAxLjg4NjY0IDE0LjExMTkgMS42MDAxIDEzLjc1ODQgMS42MDAxWiIgZmlsbD0iI2ZmZiIvPgo8cGF0aCBkPSJNNCAxMkwxMiA0TDQgMTJaIiBmaWxsPSIjZmZmIi8%2BCjxwYXRoIGQ9Ik00IDEyTDEyIDQiIHN0cm9rZT0iI2ZmZiIgc3Ryb2tlLXdpZHRoPSIxLjUiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIvPgo8L3N2Zz4K&logoColor=ffffff)](https://zread.ai/helmfile/helmfile)
声明式Helm Chart管理工具
<br />

View File

@ -17,6 +17,7 @@
[![Slack Community #helmfile](https://slack.sweetops.com/badge.svg)](https://slack.sweetops.com)
[![Documentation](https://readthedocs.org/projects/helmfile/badge/?version=latest&style=flat)](https://helmfile.readthedocs.io/en/latest/)
[![Gurubase](https://img.shields.io/badge/Gurubase-Ask%20Helmfile%20Guru-006BFF)](https://gurubase.io/g/helmfile)
[![zread](https://img.shields.io/badge/Ask_Zread-_.svg?style=flat&color=00b0aa&labelColor=000000&logo=data%3Aimage%2Fsvg%2Bxml%3Bbase64%2CPHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIHZpZXdCb3g9IjAgMCAxNiAxNiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTQuOTYxNTYgMS42MDAxSDIuMjQxNTZDMS44ODgxIDEuNjAwMSAxLjYwMTU2IDEuODg2NjQgMS42MDE1NiAyLjI0MDFWNC45NjAxQzEuNjAxNTYgNS4zMTM1NiAxLjg4ODEgNS42MDAxIDIuMjQxNTYgNS42MDAxSDQuOTYxNTZDNS4zMTUwMiA1LjYwMDEgNS42MDE1NiA1LjMxMzU2IDUuNjAxNTYgNC45NjAxVjIuMjQwMUM1LjYwMTU2IDEuODg2NjQgNS4zMTUwMiAxLjYwMDEgNC45NjE1NiAxLjYwMDFaIiBmaWxsPSIjZmZmIi8%2BCjxwYXRoIGQ9Ik00Ljk2MTU2IDEwLjM5OTlIMi4yNDE1NkMxLjg4ODEgMTAuMzk5OSAxLjYwMTU2IDEwLjY4NjQgMS42MDE1NiAxMS4wMzk5VjEzLjc1OTlDMS42MDE1NiAxNC4xMTM0IDEuODg4MSAxNC4zOTk5IDIuMjQxNTYgMTQuMzk5OUg0Ljk2MTU2QzUuMzE1MDIgMTQuMzk5OSA1LjYwMTU2IDE0LjExMzQgNS42MDE1NiAxMy43NTk5VjExLjAzOTlDNS42MDE1NiAxMC42ODY0IDUuMzE1MDIgMTAuMzk5OSA0Ljk2MTU2IDEwLjM5OTlaIiBmaWxsPSIjZmZmIi8%2BCjxwYXRoIGQ9Ik0xMy43NTg0IDEuNjAwMUgxMS4wMzg0QzEwLjY4NSAxLjYwMDEgMTAuMzk4NCAxLjg4NjY0IDEwLjM5ODQgMi4yNDAxVjQuOTYwMUMxMC4zOTg0IDUuMzEzNTYgMTAuNjg1IDUuNjAwMSAxMS4wMzg0IDUuNjAwMUgxMy43NTg0QzE0LjExMTkgNS42MDAxIDE0LjM5ODQgNS4zMTM1NiAxNC4zOTg0IDQuOTYwMVYyLjI0MDFDMTQuMzk4NCAxLjg4NjY0IDE0LjExMTkgMS42MDAxIDEzLjc1ODQgMS42MDAxWiIgZmlsbD0iI2ZmZiIvPgo8cGF0aCBkPSJNNCAxMkwxMiA0TDQgMTJaIiBmaWxsPSIjZmZmIi8%2BCjxwYXRoIGQ9Ik00IDEyTDEyIDQiIHN0cm9rZT0iI2ZmZiIgc3Ryb2tlLXdpZHRoPSIxLjUiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIvPgo8L3N2Zz4K&logoColor=ffffff)](https://zread.ai/helmfile/helmfile)
Deploy Kubernetes Helm Charts
<br />
@ -33,7 +34,9 @@ Helmfile is a declarative spec for deploying helm charts. It lets you...
* Apply CI/CD to configuration changes.
* Periodically sync to avoid skew in environments.
To avoid upgrades for each iteration of `helm`, the `helmfile` executable delegates to `helm` - as a result, `helm` must be installed.
To avoid upgrades for each iteration of `helm`, the `helmfile` executable delegates to `helm` - as a result, the following must be installed
- [helm](https://helm.sh/docs/intro/install/)
- [helm-diff](https://github.com/databus23/helm-diff)
## Highlights

19
Vagrantfile vendored
View File

@ -1,19 +0,0 @@
Vagrant.configure("2") do |config|
config.vm.box = "ubuntu/focal64"
config.vm.hostname = "minikube.box"
config.vm.provision :shell, privileged: false,
inline: <<-EOS
set -e
sudo apt-get update
sudo apt-get install -y make docker.io
sudo systemctl start docker
sudo usermod -G docker $USER
cd /vagrant/.circleci
make all
EOS
config.vm.provider "virtualbox" do |v|
v.memory = 2048
v.cpus = 2
end
end

View File

@ -76,7 +76,7 @@ func NewRootCmd(globalConfig *config.GlobalOptions) (*cobra.Command, error) {
// Set the global options for the root command.
setGlobalOptionsForRootCmd(flags, globalConfig)
flags.ParseErrorsWhitelist.UnknownFlags = true
flags.ParseErrorsAllowlist.UnknownFlags = true
globalImpl := config.NewGlobalImpl(globalConfig)

View File

@ -46,6 +46,7 @@ func NewSyncCmd(globalCfg *config.GlobalImpl) *cobra.Command {
f.BoolVar(&syncOptions.SyncReleaseLabels, "sync-release-labels", false, "sync release labels to the target release")
f.BoolVar(&syncOptions.Wait, "wait", false, `Override helmDefaults.wait setting "helm upgrade --install --wait"`)
f.BoolVar(&syncOptions.WaitForJobs, "wait-for-jobs", false, `Override helmDefaults.waitForJobs setting "helm upgrade --install --wait-for-jobs"`)
f.IntVar(&syncOptions.Timeout, "timeout", 0, `Override helmDefaults.timeout setting "helm upgrade --install --timeout" (default 0, which means no timeout)`)
f.BoolVar(&syncOptions.ReuseValues, "reuse-values", false, `Override helmDefaults.reuseValues "helm upgrade --install --reuse-values"`)
f.BoolVar(&syncOptions.ResetValues, "reset-values", false, `Override helmDefaults.reuseValues "helm upgrade --install --reset-values"`)
f.StringVar(&syncOptions.PostRenderer, "post-renderer", "", `pass --post-renderer to "helm template" or "helm upgrade --install"`)

View File

@ -194,8 +194,9 @@ helmDefaults:
skipSchemaValidation: false
# wait for k8s resources via --wait. (default false)
wait: true
# if set and --wait enabled, will retry any failed check on resource state subject to the specified number of retries (default 0)
waitRetries: 3
# DEPRECATED: waitRetries is no longer supported as the --wait-retries flag was removed from Helm.
# This configuration is ignored and preserved only for backward compatibility.
# waitRetries: 3
# if set and --wait enabled, will wait until all Jobs have been completed before marking the release as successful. It will wait for as long as --timeout (default false, Implemented in Helm3.5)
waitForJobs: true
# time in seconds to wait for any individual Kubernetes operation (like Jobs for hooks, and waits on pod/pvc/svc/deployment readiness) (default 300)
@ -318,7 +319,8 @@ releases:
# --skip-schema-validation flag to helm 'install', 'upgrade' and 'lint', starts with helm 3.16.0 (default false)
skipSchemaValidation: false
wait: true
waitRetries: 3
# DEPRECATED: waitRetries is no longer supported - see documentation above
# waitRetries: 3
waitForJobs: true
timeout: 60
recreatePods: true
@ -326,6 +328,9 @@ releases:
reuseValues: false
# set `false` to uninstall this release on sync. (default true)
installed: true
# Defines the strategy to use when updating. Possible value is:
# - "reinstallIfForbidden": Performs an uninstall before the update only if the update is forbidden (e.g., due to permission issues or conflicts).
updateStrategy: ""
# restores previous state in case of failed release (default false)
atomic: true
# when true, cleans up any new resources created during a failed release (default false)
@ -409,9 +414,16 @@ helmfiles:
# The nested-state file is locally checked-out along with the remote directory containing it.
# Therefore all the local paths in the file are resolved relative to the file
path: git::https://github.com/cloudposse/helmfiles.git@releases/kiam.yaml?ref=0.40.0
- # By default git repositories aren't updated unless the ref is updated.
# Alternatively, refer to a named ref and disable the caching.
path: git::ssh://git@github.com/cloudposse/helmfiles.git@releases/kiam.yaml?ref=main&cache=false
# If set to "Error", return an error when a subhelmfile points to a
# non-existent path. The default behavior is to print a warning and continue.
missingFileHandler: Error
missingFileHandlerConfig:
# Ignores missing git branch error so that the Debug/Info/Warn handler can treat a missing branch as non-error.
# See https://github.com/helmfile/helmfile/issues/392
ignoreMissingGitBranch: true
#
# Advanced Configuration: Environments
@ -567,7 +579,7 @@ Helmfile uses some OS environment variables to override default behaviour:
* `HELMFILE_ENVIRONMENT` - specify [Helmfile environment](https://helmfile.readthedocs.io/en/latest/#environment), it has lower priority than CLI argument `--environment`
* `HELMFILE_TEMPDIR` - specify directory to store temporary files
* `HELMFILE_UPGRADE_NOTICE_DISABLED` - expecting any non-empty value to skip the check for the latest version of Helmfile in [helmfile version](https://helmfile.readthedocs.io/en/latest/#version)
* `HELMFILE_GO_YAML_V3` - use *gopkg.in/yaml.v3* instead of *gopkg.in/yaml.v2*. It's `false` by default in Helmfile v0.x, and `true` in Helmfile v1.x.
* `HELMFILE_GO_YAML_V3` - use *go.yaml.in/yaml/v3* instead of *go.yaml.in/yaml/v2*. It's `false` by default in Helmfile v0.x, and `true` in Helmfile v1.x.
* `HELMFILE_CACHE_HOME` - specify directory to store cached files for remote operations
* `HELMFILE_FILE_PATH` - specify the path to the helmfile.yaml file
* `HELMFILE_INTERACTIVE` - enable interactive mode, expecting `true` lower case. The same as `--interactive` CLI flag
@ -577,7 +589,7 @@ Helmfile uses some OS environment variables to override default behaviour:
```
Declaratively deploy your Kubernetes manifests, Kustomize configs, and Charts as Helm releases in one shot
V1 mode = false
YAML library = gopkg.in/yaml.v3
YAML library = go.yaml.in/yaml/v3
Usage:
helmfile [command]
@ -639,6 +651,8 @@ Flags:
Use "helmfile [command] --help" for more information about a command.
```
**Note:** Each command has its own specific flags. Use `helmfile [command] --help` to see command-specific options. For example, `helmfile sync --help` shows operational flags like `--timeout`, `--wait`, and `--wait-for-jobs`.
### init
The `helmfile init` sub-command checks the dependencies required for helmfile operation, such as `helm`, `helm diff plugin`, `helm secrets plugin`, `helm helm-git plugin`, `helm s3 plugin`. When it does not exist or the version is too low, it can be installed automatically.
@ -654,6 +668,25 @@ The `helmfile sync` sub-command sync your cluster state as described in your `he
Under the covers, Helmfile executes `helm upgrade --install` for each `release` declared in the manifest, by optionally decrypting [secrets](#secrets) to be consumed as helm chart values. It also updates specified chart repositories and updates the
dependencies of any referenced local charts.
#### Common sync flags
* `--timeout SECONDS` - Override the default timeout for all releases in this sync operation. This takes precedence over `helmDefaults.timeout` and per-release `timeout` settings.
* `--wait` - Override the default wait behavior for all releases
* `--wait-for-jobs` - Override the default wait-for-jobs behavior for all releases
Examples:
```bash
# Override timeout for all releases to 10 minutes
helmfile sync --timeout 600
# Combine timeout with wait flags
helmfile sync --timeout 900 --wait --wait-for-jobs
# Target specific releases with custom timeout
helmfile sync --selector tier=backend --timeout 1200
```
For Helm 2.9+ you can use a username and password to authenticate to a remote repository.
### deps

313
go.mod
View File

@ -1,42 +1,44 @@
module github.com/helmfile/helmfile
go 1.24.2
go 1.24.6
require (
dario.cat/mergo v1.0.2
github.com/Masterminds/semver/v3 v3.4.0
github.com/Masterminds/sprig/v3 v3.3.0
github.com/aws/aws-sdk-go-v2/config v1.31.15
github.com/aws/aws-sdk-go-v2/service/s3 v1.88.7
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc
github.com/go-test/deep v1.1.1
github.com/golang/mock v1.6.0
github.com/google/go-cmp v0.7.0
github.com/gosuri/uitable v0.0.4
github.com/hashicorp/go-getter v1.7.8
github.com/hashicorp/go-getter v1.8.2
github.com/hashicorp/hcl/v2 v2.24.0
github.com/helmfile/chartify v0.24.6
github.com/helmfile/vals v0.41.2
github.com/spf13/cobra v1.9.1
github.com/spf13/pflag v1.0.6
github.com/stretchr/testify v1.10.0
github.com/helmfile/chartify v0.25.0
github.com/helmfile/vals v0.42.4
github.com/spf13/cobra v1.10.1
github.com/spf13/pflag v1.0.10
github.com/stretchr/testify v1.11.1
github.com/tatsushid/go-prettytable v0.0.0-20141013043238-ed2d14c29939
github.com/tj/assert v0.0.3
github.com/variantdev/dag v1.1.0
github.com/zclconf/go-cty v1.16.3
github.com/zclconf/go-cty v1.17.0
github.com/zclconf/go-cty-yaml v1.1.0
go.szostok.io/version v1.2.0
go.uber.org/zap v1.27.0
golang.org/x/sync v0.16.0
golang.org/x/term v0.33.0
gopkg.in/yaml.v2 v2.4.0
gopkg.in/yaml.v3 v3.0.1
helm.sh/helm/v3 v3.18.4
k8s.io/apimachinery v0.33.2
go.yaml.in/yaml/v2 v2.4.3
go.yaml.in/yaml/v3 v3.0.4
golang.org/x/sync v0.17.0
golang.org/x/term v0.36.0
helm.sh/helm/v3 v3.19.0
k8s.io/apimachinery v0.34.1
)
require (
cloud.google.com/go v0.116.0 // indirect
cloud.google.com/go/iam v1.2.2 // indirect
cloud.google.com/go/storage v1.50.0 // indirect
cloud.google.com/go v0.121.6 // indirect
cloud.google.com/go/iam v1.5.2 // indirect
cloud.google.com/go/storage v1.57.0 // indirect
filippo.io/age v1.2.1 // indirect
github.com/Azure/go-autorest v14.2.0+incompatible // indirect
github.com/Azure/go-autorest/autorest/adal v0.9.23 // indirect
@ -46,190 +48,198 @@ require (
github.com/Azure/go-autorest/tracing v0.6.0 // indirect
github.com/Masterminds/goutils v1.1.1 // indirect
github.com/a8m/envsubst v1.4.3 // indirect
github.com/aws/aws-sdk-go v1.55.7
github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d // indirect
github.com/blang/semver v3.5.1+incompatible // indirect
github.com/dimchansky/utfbom v1.1.1 // indirect
github.com/fatih/color v1.18.0
github.com/fujiwara/tfstate-lookup v1.6.0 // indirect
github.com/fujiwara/tfstate-lookup v1.7.1 // indirect
github.com/golang-jwt/jwt/v4 v4.5.2 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/google/go-querystring v1.1.0 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/googleapis/gax-go/v2 v2.14.1 // indirect
github.com/googleapis/gax-go/v2 v2.15.0 // indirect
github.com/goware/prefixer v0.0.0-20160118172347-395022866408 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/hashicorp/go-rootcerts v1.0.2 // indirect
github.com/hashicorp/go-safetemp v1.0.0 // indirect
github.com/hashicorp/go-slug v0.16.3 // indirect
github.com/hashicorp/go-slug v0.16.4 // indirect
github.com/hashicorp/go-sockaddr v1.0.7 // indirect
github.com/hashicorp/go-tfe v1.56.0 // indirect
github.com/hashicorp/go-tfe v1.84.0 // indirect
github.com/hashicorp/golang-lru v1.0.2 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/hashicorp/jsonapi v1.3.1 // indirect
github.com/hashicorp/vault/api v1.16.0 // indirect
github.com/hashicorp/hcl v1.0.1-vault-7 // indirect
github.com/hashicorp/jsonapi v1.4.3-0.20250220162346-81a76b606f3e // indirect
github.com/hashicorp/vault/api v1.22.0 // indirect
github.com/huandu/xstrings v1.5.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/itchyny/gojq v0.12.16 // indirect
github.com/jmespath/go-jmespath v0.4.0 // indirect
github.com/klauspost/compress v1.18.0 // indirect
github.com/lib/pq v1.10.9 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-runewidth v0.0.15 // indirect
github.com/mitchellh/copystructure v1.2.0 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/mitchellh/go-testing-interface v1.14.1 // indirect
github.com/mitchellh/go-wordwrap v1.0.1 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/mitchellh/reflectwalk v1.0.2 // indirect
github.com/otiai10/copy v1.14.1 // indirect
github.com/otiai10/copy v1.14.1
github.com/pkg/errors v0.9.1
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/ryanuber/go-glob v1.0.0 // indirect
github.com/shopspring/decimal v1.4.0 // indirect
github.com/sirupsen/logrus v1.9.3 // indirect
github.com/spf13/cast v1.7.0 // indirect
github.com/ulikunitz/xz v0.5.10 // indirect
go.opencensus.io v0.24.0 // indirect
github.com/ulikunitz/xz v0.5.15 // indirect
go.uber.org/atomic v1.9.0 // indirect
golang.org/x/net v0.40.0 // indirect
golang.org/x/oauth2 v0.30.0 // indirect
golang.org/x/sys v0.34.0 // indirect
golang.org/x/text v0.26.0 // indirect
golang.org/x/time v0.9.0 // indirect
google.golang.org/api v0.215.0 // indirect
google.golang.org/genproto v0.0.0-20241118233622-e639e219e697 // indirect
google.golang.org/grpc v1.68.1 // indirect
google.golang.org/protobuf v1.36.5 // indirect
golang.org/x/net v0.44.0 // indirect
golang.org/x/oauth2 v0.31.0 // indirect
golang.org/x/sys v0.37.0 // indirect
golang.org/x/text v0.29.0 // indirect
golang.org/x/time v0.13.0 // indirect
google.golang.org/api v0.252.0 // indirect
google.golang.org/genproto v0.0.0-20250603155806-513f23925822 // indirect
google.golang.org/grpc v1.75.1 // indirect
google.golang.org/protobuf v1.36.10 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect
sigs.k8s.io/yaml v1.4.0 // indirect
sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect
sigs.k8s.io/yaml v1.6.0 // indirect
)
require (
al.essio.dev/pkg/shellescape v1.5.1 // indirect
cel.dev/expr v0.19.1 // indirect
cloud.google.com/go/auth v0.13.0 // indirect
cloud.google.com/go/auth/oauth2adapt v0.2.6 // indirect
cloud.google.com/go/compute/metadata v0.6.0 // indirect
cloud.google.com/go/kms v1.20.5 // indirect
cloud.google.com/go/longrunning v0.6.2 // indirect
cloud.google.com/go/monitoring v1.21.2 // indirect
cloud.google.com/go/secretmanager v1.14.3 // indirect
al.essio.dev/pkg/shellescape v1.6.0 // indirect
cel.dev/expr v0.24.0 // indirect
cloud.google.com/go/auth v0.17.0 // indirect
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
cloud.google.com/go/compute/metadata v0.9.0 // indirect
cloud.google.com/go/kms v1.23.0 // indirect
cloud.google.com/go/longrunning v0.6.7 // indirect
cloud.google.com/go/monitoring v1.24.2 // indirect
cloud.google.com/go/secretmanager v1.15.0 // indirect
filippo.io/edwards25519 v1.1.0 // indirect
github.com/1Password/connect-sdk-go v1.5.3 // indirect
github.com/1password/onepassword-sdk-go v0.3.0 // indirect
github.com/1password/onepassword-sdk-go v0.3.1 // indirect
github.com/AlecAivazis/survey/v2 v2.3.6 // indirect
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.10.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1 // indirect
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage v1.3.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.3.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azsecrets v1.3.1 // indirect
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.1.1 // indirect
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.19.1 // indirect
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.12.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 // indirect
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage v1.8.1 // indirect
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.4.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azsecrets v1.4.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.2.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.1.0 // indirect
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect
github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2 // indirect
github.com/AzureAD/microsoft-authentication-library-for-go v1.5.0 // indirect
github.com/DopplerHQ/cli v0.5.11-0.20230908185655-7aef4713e1a4 // indirect
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.25.0 // indirect
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.48.1 // indirect
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.48.1 // indirect
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.29.0 // indirect
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.53.0 // indirect
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.53.0 // indirect
github.com/MakeNowJust/heredoc v1.0.0 // indirect
github.com/Masterminds/squirrel v1.5.4 // indirect
github.com/ProtonMail/go-crypto v1.1.3 // indirect
github.com/ProtonMail/go-crypto v1.3.0 // indirect
github.com/agext/levenshtein v1.2.3 // indirect
github.com/antchfx/jsonquery v1.3.6 // indirect
github.com/antchfx/xpath v1.3.4 // indirect
github.com/antchfx/xpath v1.3.5 // indirect
github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect
github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de // indirect
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect
github.com/atotto/clipboard v0.1.4 // indirect
github.com/aws/aws-sdk-go-v2 v1.36.3 // indirect
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.10 // indirect
github.com/aws/aws-sdk-go-v2/config v1.29.14 // indirect
github.com/aws/aws-sdk-go-v2/credentials v1.17.67 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30 // indirect
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.72 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 // indirect
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.34 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.7.0 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.15 // indirect
github.com/aws/aws-sdk-go-v2/service/kms v1.37.7 // indirect
github.com/aws/aws-sdk-go-v2/service/s3 v1.79.2 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.25.3 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.1 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.33.19 // indirect
github.com/aws/smithy-go v1.22.2 // indirect
github.com/aws/aws-sdk-go-v2 v1.39.4 // indirect
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.2 // indirect
github.com/aws/aws-sdk-go-v2/credentials v1.18.19 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.11 // indirect
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.19.9 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.11 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.11 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 // indirect
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.11 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.2 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.2 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.11 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.11 // indirect
github.com/aws/aws-sdk-go-v2/service/kms v1.45.6 // indirect
github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.39.6 // indirect
github.com/aws/aws-sdk-go-v2/service/ssm v1.65.1 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.29.8 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.3 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.38.9 // indirect
github.com/aws/smithy-go v1.23.1 // indirect
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/blang/semver/v4 v4.0.0 // indirect
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/census-instrumentation/opencensus-proto v0.4.1 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/chai2010/gettext-go v1.0.2 // indirect
github.com/cloudflare/circl v1.6.1 // indirect
github.com/cncf/xds/go v0.0.0-20240905190251-b4127c9b8d78 // indirect
github.com/containerd/containerd v1.7.27 // indirect
github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443 // indirect
github.com/containerd/containerd v1.7.28 // indirect
github.com/containerd/errdefs v0.3.0 // indirect
github.com/containerd/log v0.1.0 // indirect
github.com/containerd/platforms v0.2.1 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.6 // indirect
github.com/cyberark/conjur-api-go v0.13.0 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect
github.com/cyberark/conjur-api-go v0.13.7 // indirect
github.com/danieljoos/wincred v1.2.2 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/dylibso/observe-sdk/go v0.0.0-20240819160327-2d926c5d788a // indirect
github.com/emicklei/go-restful/v3 v3.12.1 // indirect
github.com/envoyproxy/go-control-plane v0.13.1 // indirect
github.com/envoyproxy/protoc-gen-validate v1.1.0 // indirect
github.com/emicklei/go-restful/v3 v3.12.2 // indirect
github.com/envoyproxy/go-control-plane/envoy v1.32.4 // indirect
github.com/envoyproxy/protoc-gen-validate v1.2.1 // indirect
github.com/evanphx/json-patch v5.9.11+incompatible // indirect
github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f // indirect
github.com/extism/go-sdk v1.7.0 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/fxamacker/cbor/v2 v2.7.0 // indirect
github.com/getsops/gopgagent v0.0.0-20240527072608-0c14999532fe // indirect
github.com/getsops/sops/v3 v3.9.2 // indirect
github.com/fxamacker/cbor/v2 v2.9.0 // indirect
github.com/getsops/gopgagent v0.0.0-20241224165529-7044f28e491e // indirect
github.com/getsops/sops/v3 v3.11.0 // indirect
github.com/ghodss/yaml v1.0.0 // indirect
github.com/go-errors/errors v1.4.2 // indirect
github.com/go-gorp/gorp/v3 v3.1.0 // indirect
github.com/go-jose/go-jose/v4 v4.0.5 // indirect
github.com/go-logr/logr v1.4.2 // indirect
github.com/go-jose/go-jose/v4 v4.1.1 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-openapi/analysis v0.23.0 // indirect
github.com/go-openapi/errors v0.22.1 // indirect
github.com/go-openapi/jsonpointer v0.21.0 // indirect
github.com/go-openapi/jsonreference v0.21.0 // indirect
github.com/go-openapi/loads v0.22.0 // indirect
github.com/go-openapi/runtime v0.28.0 // indirect
github.com/go-openapi/spec v0.21.0 // indirect
github.com/go-openapi/strfmt v0.23.0 // indirect
github.com/go-openapi/swag v0.23.1 // indirect
github.com/go-openapi/validate v0.24.0 // indirect
github.com/go-openapi/analysis v0.24.0 // indirect
github.com/go-openapi/errors v0.22.3 // indirect
github.com/go-openapi/jsonpointer v0.22.1 // indirect
github.com/go-openapi/jsonreference v0.21.2 // indirect
github.com/go-openapi/loads v0.23.1 // indirect
github.com/go-openapi/runtime v0.29.0 // indirect
github.com/go-openapi/spec v0.22.0 // indirect
github.com/go-openapi/strfmt v0.24.0 // indirect
github.com/go-openapi/swag v0.24.1 // indirect
github.com/go-openapi/swag/cmdutils v0.24.0 // indirect
github.com/go-openapi/swag/conv v0.25.1 // indirect
github.com/go-openapi/swag/fileutils v0.25.1 // indirect
github.com/go-openapi/swag/jsonname v0.25.1 // indirect
github.com/go-openapi/swag/jsonutils v0.25.1 // indirect
github.com/go-openapi/swag/loading v0.25.1 // indirect
github.com/go-openapi/swag/mangling v0.25.1 // indirect
github.com/go-openapi/swag/netutils v0.24.0 // indirect
github.com/go-openapi/swag/stringutils v0.25.1 // indirect
github.com/go-openapi/swag/typeutils v0.25.1 // indirect
github.com/go-openapi/swag/yamlutils v0.25.1 // indirect
github.com/go-openapi/validate v0.25.0 // indirect
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
github.com/gobwas/glob v0.2.3 // indirect
github.com/goccy/go-yaml v1.17.1 // indirect
github.com/godbus/dbus/v5 v5.1.0 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang-jwt/jwt/v5 v5.2.2 // indirect
github.com/golang-jwt/jwt/v5 v5.3.0 // indirect
github.com/google/btree v1.1.3 // indirect
github.com/google/gnostic-models v0.6.9 // indirect
github.com/google/gnostic-models v0.7.0 // indirect
github.com/google/go-jsonnet v0.20.0 // indirect
github.com/google/s2a-go v0.1.8 // indirect
github.com/google/s2a-go v0.1.9 // indirect
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.4 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 // indirect
github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 // indirect
github.com/hashicorp/go-retryablehttp v0.7.7 // indirect
github.com/hashicorp/go-secure-stdlib/parseutil v0.1.8 // indirect
github.com/hashicorp/aws-sdk-go-base/v2 v2.0.0-beta.65 // indirect
github.com/hashicorp/go-retryablehttp v0.7.8 // indirect
github.com/hashicorp/go-secure-stdlib/parseutil v0.2.0 // indirect
github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 // indirect
github.com/hashicorp/go-version v1.6.0 // indirect
github.com/hashicorp/hcp-sdk-go v0.144.0 // indirect
github.com/hashicorp/go-version v1.7.0 // indirect
github.com/hashicorp/hcp-sdk-go v0.162.0 // indirect
github.com/hokaccha/go-prettyjson v0.0.0-20211117102719-0474bc63780f // indirect
github.com/ianlancetaylor/demangle v0.0.0-20240805132620-81f5be970eca // indirect
github.com/itchyny/timefmt-go v0.1.6 // indirect
github.com/jmoiron/sqlx v1.4.0 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
github.com/kylelemons/godebug v1.1.0 // indirect
@ -237,14 +247,13 @@ require (
github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect
github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/mailru/easyjson v0.9.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db // indirect
github.com/moby/spdystream v0.5.0 // indirect
github.com/moby/term v0.5.2 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect
github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 // indirect
github.com/muesli/termenv v0.15.1 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
@ -260,7 +269,9 @@ require (
github.com/rivo/uniseg v0.4.7 // indirect
github.com/rubenv/sql-migrate v1.8.0 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.33 // indirect
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 // indirect
github.com/spiffe/go-spiffe/v2 v2.5.0 // indirect
github.com/tetratelabs/wabin v0.0.0-20230304001439-f6f874872834 // indirect
github.com/tetratelabs/wazero v1.9.0 // indirect
github.com/tidwall/gjson v1.18.0 // indirect
@ -268,44 +279,48 @@ require (
github.com/tidwall/pretty v1.2.0 // indirect
github.com/uber/jaeger-client-go v2.30.0+incompatible // indirect
github.com/uber/jaeger-lib v2.4.1+incompatible // indirect
github.com/urfave/cli v1.22.16 // indirect
github.com/urfave/cli v1.22.17 // indirect
github.com/x448/float16 v0.8.4 // indirect
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
github.com/xlab/treeprint v1.2.0 // indirect
github.com/yandex-cloud/go-genproto v0.29.0 // indirect
github.com/yandex-cloud/go-sdk v0.22.0 // indirect
github.com/zalando/go-keyring v0.2.6 // indirect
go.mongodb.org/mongo-driver v1.14.0 // indirect
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
go.opentelemetry.io/contrib/detectors/gcp v1.29.0 // indirect
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.58.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 // indirect
go.opentelemetry.io/otel v1.33.0 // indirect
go.opentelemetry.io/otel/metric v1.33.0 // indirect
go.opentelemetry.io/otel/sdk v1.33.0 // indirect
go.opentelemetry.io/otel/sdk/metric v1.32.0 // indirect
go.opentelemetry.io/otel/trace v1.33.0 // indirect
go.opentelemetry.io/proto/otlp v1.4.0 // indirect
github.com/zeebo/errs v1.4.0 // indirect
go.mongodb.org/mongo-driver v1.17.4 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/contrib/detectors/gcp v1.36.0 // indirect
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect
go.opentelemetry.io/otel v1.38.0 // indirect
go.opentelemetry.io/otel/metric v1.38.0 // indirect
go.opentelemetry.io/otel/sdk v1.38.0 // indirect
go.opentelemetry.io/otel/sdk/metric v1.38.0 // indirect
go.opentelemetry.io/otel/trace v1.38.0 // indirect
go.opentelemetry.io/proto/otlp v1.5.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
golang.org/x/crypto v0.39.0 // indirect
golang.org/x/mod v0.25.0 // indirect
golang.org/x/tools v0.33.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20241223144023-3abc09e42ca8 // indirect
google.golang.org/grpc/stats/opentelemetry v0.0.0-20240907200651-3ffb98b2c93a // indirect
golang.org/x/crypto v0.42.0 // indirect
golang.org/x/mod v0.27.0 // indirect
golang.org/x/tools v0.36.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20250818200422-3122310a409c // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797 // indirect
gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect
gopkg.in/gookit/color.v1 v1.1.6 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
k8s.io/api v0.33.2 // indirect
k8s.io/apiextensions-apiserver v0.33.2 // indirect
k8s.io/cli-runtime v0.33.2 // indirect
k8s.io/client-go v0.33.2 // indirect
k8s.io/component-base v0.33.2 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
k8s.io/api v0.34.1 // indirect
k8s.io/apiextensions-apiserver v0.34.0 // indirect
k8s.io/cli-runtime v0.34.0 // indirect
k8s.io/client-go v0.34.1 // indirect
k8s.io/component-base v0.34.0 // indirect
k8s.io/klog/v2 v2.130.1 // indirect
k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff // indirect
k8s.io/kubectl v0.33.2 // indirect
k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 // indirect
k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b // indirect
k8s.io/kubectl v0.34.0 // indirect
k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 // indirect
oras.land/oras-go/v2 v2.6.0 // indirect
sigs.k8s.io/kustomize/api v0.19.0 // indirect
sigs.k8s.io/kustomize/kyaml v0.19.0 // indirect
sigs.k8s.io/kustomize/api v0.20.1 // indirect
sigs.k8s.io/kustomize/kyaml v0.20.1 // indirect
sigs.k8s.io/randfill v1.0.0 // indirect
sigs.k8s.io/structured-merge-diff/v4 v4.6.0 // indirect
sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect
)

2168
go.sum

File diff suppressed because it is too large Load Diff

View File

@ -199,7 +199,7 @@ func (a *App) Diff(c DiffConfigProvider) error {
}
if c.DetailedExitcode() && (len(allDiffDetectedErrs) > 0 || affectedAny) {
// We take the first release error w/ exit status 2 (although all the defered errs should have exit status 2)
// We take the first release error w/ exit status 2 (although all the deferred errs should have exit status 2)
// to just let helmfile itself to exit with 2
// See https://github.com/roboll/helmfile/issues/749
code := 2
@ -345,8 +345,7 @@ func (a *App) Fetch(c FetchConfigProvider) error {
OutputDir: c.OutputDir(),
OutputDirTemplate: c.OutputDirTemplate(),
Concurrency: c.Concurrency(),
}, func() {
})
}, func() {})
if prepErr != nil {
errs = append(errs, prepErr)
@ -681,7 +680,7 @@ func (a *App) within(dir string, do func() error) error {
prev, err := a.fs.Getwd()
if err != nil {
return fmt.Errorf("failed getting current working direcotyr: %v", err)
return fmt.Errorf("failed getting current working directory: %v", err)
}
absDir, err := a.fs.Abs(dir)
@ -785,7 +784,7 @@ func createHelmKey(bin, kubectx string) helmKey {
//
// This is currently used for running all the helm commands for reconciling releases. But this may change in the future
// once we enable each release to have its own helm binary/version.
func (a *App) getHelm(st *state.HelmState) helmexec.Interface {
func (a *App) getHelm(st *state.HelmState) (helmexec.Interface, error) {
a.helmsMutex.Lock()
defer a.helmsMutex.Unlock()
@ -800,14 +799,18 @@ func (a *App) getHelm(st *state.HelmState) helmexec.Interface {
key := createHelmKey(bin, kubectx)
if _, ok := a.helms[key]; !ok {
a.helms[key] = helmexec.New(bin, helmexec.HelmExecOptions{EnableLiveOutput: a.EnableLiveOutput, DisableForceUpdate: a.DisableForceUpdate}, a.Logger, kubeconfig, kubectx, &helmexec.ShellRunner{
exec, err := helmexec.New(bin, helmexec.HelmExecOptions{EnableLiveOutput: a.EnableLiveOutput, DisableForceUpdate: a.DisableForceUpdate}, a.Logger, kubeconfig, kubectx, &helmexec.ShellRunner{
Logger: a.Logger,
Ctx: a.ctx,
StripArgsValuesOnExitError: a.StripArgsValuesOnExitError,
})
if err != nil {
return nil, err
}
a.helms[key] = exec
}
return a.helms[key]
return a.helms[key], nil
}
func (a *App) visitStates(fileOrDir string, defOpts LoadOpts, converge func(*state.HelmState) (bool, []error)) error {
@ -959,7 +962,10 @@ var (
func (a *App) ForEachState(do func(*Run) (bool, []error), includeTransitiveNeeds bool, o ...LoadOption) error {
ctx := NewContext()
err := a.visitStatesWithSelectorsAndRemoteSupport(a.FileOrDir, func(st *state.HelmState) (bool, []error) {
helm := a.getHelm(st)
helm, err := a.getHelm(st)
if err != nil {
return false, []error{err}
}
run, err := NewRun(st, helm, ctx)
if err != nil {

View File

@ -415,4 +415,28 @@ releases:
},
})
})
t.Run("show diff on changed selected release with reinstall", func(t *testing.T) {
check(t, testcase{
helmfile: `
releases:
- name: a
chart: incubator/raw
namespace: default
updateStrategy: reinstallIfForbidden
- name: b
chart: incubator/raw
namespace: default
`,
selectors: []string{"name=a"},
lists: map[exectest.ListKey]string{
{Filter: "^a$", Flags: listFlags("default", "default")}: `NAME REVISION UPDATED STATUS CHART APP VERSION NAMESPACE
foo 4 Fri Nov 1 08:40:07 2019 DEPLOYED raw-3.1.0 3.1.0 default
`,
},
diffed: []exectest.Release{
{Name: "a", Flags: []string{"--kube-context", "default", "--namespace", "default", "--reset-values"}},
},
})
})
}

View File

@ -411,7 +411,7 @@ releases:
})
}
t.Run("fail due to unknown field with gopkg.in/yaml.v3", func(t *testing.T) {
t.Run("fail due to unknown field with go.yaml.in/yaml/v3", func(t *testing.T) {
check(t, testcase{
GoYamlV3: true,
error: `in ./helmfile.yaml: failed to read helmfile.yaml: reading document at index 1. Started seeing this since Helmfile v1? Add the .gotmpl file extension: yaml: unmarshal errors:
@ -419,7 +419,7 @@ releases:
})
})
t.Run("fail due to unknown field with gopkg.in/yaml.v2", func(t *testing.T) {
t.Run("fail due to unknown field with go.yaml.in/yaml/v2", func(t *testing.T) {
check(t, testcase{
GoYamlV3: false,
error: `in ./helmfile.yaml: failed to read helmfile.yaml: reading document at index 1. Started seeing this since Helmfile v1? Add the .gotmpl file extension: yaml: unmarshal errors:

View File

@ -220,6 +220,97 @@ releases:
}
}
func TestUpdateStrategyParamValidation(t *testing.T) {
cases := []struct {
files map[string]string
updateStrategy string
isValid bool
}{
{map[string]string{
"/path/to/helmfile.yaml": `releases:
- name: zipkin
chart: stable/zipkin
updateStrategy: reinstallIfForbidden
`},
"reinstallIfForbidden",
true},
{map[string]string{
"/path/to/helmfile.yaml": `releases:
- name: zipkin
chart: stable/zipkin
updateStrategy: reinstallIfForbidden
`},
"reinstallIfForbidden",
true},
{map[string]string{
"/path/to/helmfile.yaml": `releases:
- name: zipkin
chart: stable/zipkin
updateStrategy:
`},
"",
true},
{map[string]string{
"/path/to/helmfile.yaml": `releases:
- name: zipkin
chart: stable/zipkin
updateStrategy: foo
`},
"foo",
false},
{map[string]string{
"/path/to/helmfile.yaml": `releases:
- name: zipkin
chart: stable/zipkin
updateStrategy: reinstal
`},
"reinstal",
false},
{map[string]string{
"/path/to/helmfile.yaml": `releases:
- name: zipkin
chart: stable/zipkin
updateStrategy: reinstall1
`},
"reinstall1",
false},
}
for idx, c := range cases {
fs := testhelper.NewTestFs(c.files)
app := &App{
OverrideHelmBinary: DefaultHelmBinary,
OverrideKubeContext: "default",
Logger: newAppTestLogger(),
Namespace: "",
Env: "default",
FileOrDir: "helmfile.yaml",
}
expectNoCallsToHelm(app)
app = injectFs(app, fs)
err := app.ForEachState(
Noop,
false,
SetFilter(true),
)
if c.isValid && err != nil {
t.Errorf("[case: %d] Unexpected error for valid case: %v", idx, err)
} else if !c.isValid {
var invalidUpdateStrategy state.InvalidUpdateStrategyError
invalidUpdateStrategy.UpdateStrategy = c.updateStrategy
if err == nil {
t.Errorf("[case: %d] Expected error for invalid case", idx)
} else if !strings.Contains(err.Error(), invalidUpdateStrategy.Error()) {
t.Errorf("[case: %d] Unexpected error returned for invalid case\ngot: %v\nexpected underlying error: %s", idx, err, invalidUpdateStrategy.Error())
}
}
}
}
func TestVisitDesiredStatesWithReleasesFiltered_Issue1008_MissingNonDefaultEnvInBase(t *testing.T) {
files := map[string]string{
"/path/to/base.yaml": `
@ -2492,9 +2583,12 @@ func (mock *mockRunner) Execute(cmd string, args []string, env map[string]string
return []byte{}, nil
}
func MockExecer(logger *zap.SugaredLogger, kubeContext string) helmexec.Interface {
execer := helmexec.New("helm", helmexec.HelmExecOptions{}, logger, "", kubeContext, &mockRunner{})
return execer
func MockExecer(logger *zap.SugaredLogger, kubeContext string) (helmexec.Interface, error) {
execer, err := helmexec.New("helm", helmexec.HelmExecOptions{}, logger, "", kubeContext, &mockRunner{})
if err != nil {
return nil, err
}
return execer, nil
}
// mocking helmexec.Interface
@ -3073,6 +3167,97 @@ baz 4 Fri Nov 1 08:40:07 2019 DEPLOYED mychart3-3.1.0 3.1.0 defau
concurrency: 1,
},
//
// install with upgrade with reinstallIfForbidden
//
{
name: "install-with-upgrade-with-reinstallIfForbidden",
loc: location(),
files: map[string]string{
"/path/to/helmfile.yaml": `
releases:
- name: baz
chart: stable/mychart3
disableValidationOnInstall: true
updateStrategy: reinstallIfForbidden
- name: foo
chart: stable/mychart1
disableValidationOnInstall: true
needs:
- bar
- name: bar
chart: stable/mychart2
disableValidation: true
updateStrategy: reinstallIfForbidden
`,
},
diffs: map[exectest.DiffKey]error{
{Name: "baz", Chart: "stable/mychart3", Flags: "--kube-context default --reset-values --detailed-exitcode"}: helmexec.ExitError{Code: 2},
{Name: "foo", Chart: "stable/mychart1", Flags: "--disable-validation --kube-context default --reset-values --detailed-exitcode"}: helmexec.ExitError{Code: 2},
{Name: "bar", Chart: "stable/mychart2", Flags: "--disable-validation --kube-context default --reset-values --detailed-exitcode"}: helmexec.ExitError{Code: 2},
},
lists: map[exectest.ListKey]string{
{Filter: "^foo$", Flags: listFlags("", "default")}: ``,
{Filter: "^bar$", Flags: listFlags("", "default")}: `NAME REVISION UPDATED STATUS CHART APP VERSION NAMESPACE
bar 4 Fri Nov 1 08:40:07 2019 DEPLOYED mychart2-3.1.0 3.1.0 default
`,
{Filter: "^baz$", Flags: listFlags("", "default")}: `NAME REVISION UPDATED STATUS CHART APP VERSION NAMESPACE
baz 4 Fri Nov 1 08:40:07 2019 DEPLOYED mychart3-3.1.0 3.1.0 default
`,
},
upgraded: []exectest.Release{
{Name: "baz", Flags: []string{"--kube-context", "default"}},
{Name: "bar", Flags: []string{"--kube-context", "default"}},
{Name: "foo", Flags: []string{"--kube-context", "default"}},
},
deleted: []exectest.Release{},
concurrency: 1,
},
//
// install with upgrade and --skip-diff-on-install with reinstallIfForbidden
//
{
name: "install-with-upgrade-with-skip-diff-on-install-with-reinstallIfForbidden",
loc: location(),
skipDiffOnInstall: true,
files: map[string]string{
"/path/to/helmfile.yaml": `
releases:
- name: baz
chart: stable/mychart3
disableValidationOnInstall: true
updateStrategy: reinstallIfForbidden
- name: foo
chart: stable/mychart1
disableValidationOnInstall: true
needs:
- bar
- name: bar
chart: stable/mychart2
disableValidation: true
updateStrategy: reinstallIfForbidden
`,
},
diffs: map[exectest.DiffKey]error{
{Name: "baz", Chart: "stable/mychart3", Flags: "--kube-context default --reset-values --detailed-exitcode"}: helmexec.ExitError{Code: 2},
{Name: "bar", Chart: "stable/mychart2", Flags: "--disable-validation --kube-context default --reset-values --detailed-exitcode"}: helmexec.ExitError{Code: 2},
},
lists: map[exectest.ListKey]string{
{Filter: "^foo$", Flags: listFlags("", "default")}: ``,
{Filter: "^bar$", Flags: listFlags("", "default")}: `NAME REVISION UPDATED STATUS CHART APP VERSION NAMESPACE
bar 4 Fri Nov 1 08:40:07 2019 DEPLOYED mychart2-3.1.0 3.1.0 default
`,
{Filter: "^baz$", Flags: listFlags("", "default")}: `NAME REVISION UPDATED STATUS CHART APP VERSION NAMESPACE
baz 4 Fri Nov 1 08:40:07 2019 DEPLOYED mychart3-3.1.0 3.1.0 default
`,
},
upgraded: []exectest.Release{
{Name: "baz", Flags: []string{"--kube-context", "default"}},
{Name: "bar", Flags: []string{"--kube-context", "default"}},
{Name: "foo", Flags: []string{"--kube-context", "default"}},
},
concurrency: 1,
},
//
// upgrades
//
{
@ -3769,7 +3954,7 @@ releases:
}
for flagIdx := range wantDeletes[relIdx].Flags {
if wantDeletes[relIdx].Flags[flagIdx] != helm.Deleted[relIdx].Flags[flagIdx] {
t.Errorf("releaes[%d].flags[%d]: got %v, want %v", relIdx, flagIdx, helm.Deleted[relIdx].Flags[flagIdx], wantDeletes[relIdx].Flags[flagIdx])
t.Errorf("releases[%d].flags[%d]: got %v, want %v", relIdx, flagIdx, helm.Deleted[relIdx].Flags[flagIdx], wantDeletes[relIdx].Flags[flagIdx])
}
}
}
@ -4161,21 +4346,21 @@ releases:
}
func TestSetValuesTemplate(t *testing.T) {
t.Run("with gopkg.in/yaml.v3", func(t *testing.T) {
t.Run("with go.yaml.in/yaml/v3", func(t *testing.T) {
testSetValuesTemplate(t, true)
})
t.Run("with gopkg.in/yaml.v2", func(t *testing.T) {
t.Run("with go.yaml.in/yaml/v2", func(t *testing.T) {
testSetValuesTemplate(t, false)
})
}
func TestSetStringValuesTemplate(t *testing.T) {
t.Run("with gopkg.in/yaml.v3", func(t *testing.T) {
t.Run("with go.yaml.in/yaml/v3", func(t *testing.T) {
testSetStringValuesTemplate(t, true)
})
t.Run("with gopkg.in/yaml.v2", func(t *testing.T) {
t.Run("with go.yaml.in/yaml/v2", func(t *testing.T) {
testSetStringValuesTemplate(t, false)
})
}

View File

@ -5,6 +5,7 @@ import (
"errors"
"fmt"
"path/filepath"
"slices"
"dario.cat/mergo"
"github.com/helmfile/vals"
@ -34,7 +35,7 @@ type desiredStateLoader struct {
chart string
fs *filesystem.FileSystem
getHelm func(*state.HelmState) helmexec.Interface
getHelm func(*state.HelmState) (helmexec.Interface, error)
remote *remote.Remote
logger *zap.SugaredLogger
@ -285,6 +286,18 @@ func (ld *desiredStateLoader) load(env, overrodeEnv *environment.Environment, ba
}
}
// Validate updateStrategy value if set in the releases
for i := range finalState.Releases {
if finalState.Releases[i].UpdateStrategy != "" {
if !slices.Contains(state.ValidUpdateStrategyValues, finalState.Releases[i].UpdateStrategy) {
return nil, &state.StateLoadError{
Msg: fmt.Sprintf("failed to read %s", finalState.FilePath),
Cause: &state.InvalidUpdateStrategyError{UpdateStrategy: finalState.Releases[i].UpdateStrategy},
}
}
}
}
finalState.OrginReleases = finalState.Releases
return finalState, nil
}

View File

@ -17,9 +17,9 @@ import (
)
const (
HelmRequiredVersion = "v3.17.3"
HelmDiffRecommendedVersion = "v3.12.3"
HelmRecommendedVersion = "v3.18.4"
HelmRequiredVersion = "v3.18.6"
HelmDiffRecommendedVersion = "v3.13.1"
HelmRecommendedVersion = "v3.19.0"
HelmSecretsRecommendedVersion = "v4.6.5"
HelmGitRecommendedVersion = "v1.3.0"
HelmS3RecommendedVersion = "v0.16.3"
@ -163,7 +163,10 @@ func (h *HelmfileInit) WhetherContinue(ask string) error {
func (h *HelmfileInit) CheckHelmPlugins() error {
settings := cli.New()
helm := helmexec.New(h.helmBinary, helmexec.HelmExecOptions{}, h.logger, "", "", h.runner)
helm, err := helmexec.New(h.helmBinary, helmexec.HelmExecOptions{}, h.logger, "", "", h.runner)
if err != nil {
return err
}
for _, p := range helmPlugins {
pluginVersion, err := helmexec.GetPluginVersion(p.name, settings.PluginsDirectory)
if err != nil {

View File

@ -0,0 +1,11 @@
processing file "helmfile.yaml" in directory "."
changing working directory to "/path/to"
merged environment: &{default map[] map[]}
1 release(s) matching name=a found in helmfile.yaml
processing 1 groups of releases in this order:
GROUP RELEASES
1 default/default/a
processing releases in group 1/1: default/default/a
changing working directory back to "/path/to"

View File

@ -0,0 +1,35 @@
processing file "helmfile.yaml" in directory "."
changing working directory to "/path/to"
merged environment: &{default map[] map[]}
3 release(s) found in helmfile.yaml
Affected releases are:
bar (stable/mychart2) UPDATED
baz (stable/mychart3) UPDATED
foo (stable/mychart1) UPDATED
invoking preapply hooks for 2 groups of releases in this order:
GROUP RELEASES
1 default//foo
2 default//baz, default//bar
invoking preapply hooks for releases in group 1/2: default//foo
invoking preapply hooks for releases in group 2/2: default//baz, default//bar
processing 2 groups of releases in this order:
GROUP RELEASES
1 default//baz, default//bar
2 default//foo
processing releases in group 1/2: default//baz, default//bar
update strategy - sync success
update strategy - sync success
processing releases in group 2/2: default//foo
getting deployed release version failed: Failed to get the version for: mychart1
UPDATED RELEASES:
NAME NAMESPACE CHART VERSION DURATION
baz stable/mychart3 3.1.0 0s
bar stable/mychart2 3.1.0 0s
foo stable/mychart1 0s
changing working directory back to "/path/to"

View File

@ -0,0 +1,35 @@
processing file "helmfile.yaml" in directory "."
changing working directory to "/path/to"
merged environment: &{default map[] map[]}
3 release(s) found in helmfile.yaml
Affected releases are:
bar (stable/mychart2) UPDATED
baz (stable/mychart3) UPDATED
foo (stable/mychart1) UPDATED
invoking preapply hooks for 2 groups of releases in this order:
GROUP RELEASES
1 default//foo
2 default//baz, default//bar
invoking preapply hooks for releases in group 1/2: default//foo
invoking preapply hooks for releases in group 2/2: default//baz, default//bar
processing 2 groups of releases in this order:
GROUP RELEASES
1 default//baz, default//bar
2 default//foo
processing releases in group 1/2: default//baz, default//bar
update strategy - sync success
update strategy - sync success
processing releases in group 2/2: default//foo
getting deployed release version failed: Failed to get the version for: mychart1
UPDATED RELEASES:
NAME NAMESPACE CHART VERSION DURATION
baz stable/mychart3 3.1.0 0s
bar stable/mychart2 3.1.0 0s
foo stable/mychart1 0s
changing working directory back to "/path/to"

View File

@ -24,6 +24,8 @@ type SyncOptions struct {
WaitRetries int
// WaitForJobs is the wait for jobs flag
WaitForJobs bool
// Timeout is the timeout flag in seconds
Timeout int
// ReuseValues is true if the helm command should reuse the values
ReuseValues bool
// ResetValues is true if helm command should reset values to charts' default
@ -124,6 +126,11 @@ func (t *SyncImpl) WaitForJobs() bool {
return t.SyncOptions.WaitForJobs
}
// Timeout returns the timeout
func (t *SyncImpl) Timeout() int {
return t.SyncOptions.Timeout
}
// ReuseValues returns the ReuseValues.
func (t *SyncImpl) ReuseValues() bool {
if !t.ResetValues() {

View File

@ -57,6 +57,7 @@ type Release struct {
type Affected struct {
Upgraded []*Release
Reinstalled []*Release
Deleted []*Release
Failed []*Release
}
@ -107,7 +108,24 @@ func (helm *Helm) RegistryLogin(name, username, password, caFile, certFile, keyF
return nil
}
func (helm *Helm) SyncRelease(context helmexec.HelmContext, name, chart, namespace string, flags ...string) error {
if strings.Contains(name, "error") {
if strings.Contains(name, "forbidden") {
releaseExists := false
for _, release := range helm.Releases {
if release.Name == name {
releaseExists = true
}
}
releaseDeleted := false
for _, release := range helm.Deleted {
if release.Name == name {
releaseDeleted = true
}
}
// Only fail if the release is present in the helm.Releases to simulate a forbidden update if it exists
if releaseExists && !releaseDeleted {
return fmt.Errorf("cannot patch %q with kind StatefulSet: StatefulSet.apps %q is invalid: spec: Forbidden: updates to statefulset spec for fields other than 'replicas', 'ordinals', 'template', 'updateStrategy', 'persistentVolumeClaimRetentionPolicy' and 'minReadySeconds' are forbidden", name, name)
}
} else if strings.Contains(name, "error") {
return errors.New("error")
}
helm.sync(helm.ReleasesMutex, func() {

View File

@ -6,6 +6,8 @@ import (
"os"
"path/filepath"
"time"
copyDir "github.com/otiai10/copy"
)
type fileStat struct {
@ -36,6 +38,7 @@ type FileSystem struct {
Chdir func(string) error
Abs func(string) (string, error)
EvalSymlinks func(string) (string, error)
CopyDir func(src, dst string) error
}
func DefaultFileSystem() *FileSystem {
@ -55,6 +58,7 @@ func DefaultFileSystem() *FileSystem {
dfs.DirectoryExistsAt = dfs.directoryExistsDefault
dfs.FileExists = dfs.fileExistsDefault
dfs.Abs = dfs.absDefault
dfs.CopyDir = dfs.copyDirDefault
return &dfs
}
@ -100,6 +104,9 @@ func FromFileSystem(params FileSystem) *FileSystem {
if params.Dir != nil {
dfs.Dir = params.Dir
}
if params.CopyDir != nil {
dfs.CopyDir = params.CopyDir
}
return dfs
}
@ -180,3 +187,8 @@ func (filesystem *FileSystem) absDefault(path string) (string, error) {
}
return filepath.Abs(path)
}
// copyDirDefault recursively copies a directory tree, preserving permissions.
func (filesystem *FileSystem) copyDirDefault(src string, dst string) error {
return copyDir.Copy(src, dst, copyDir.Options{Sync: true})
}

View File

@ -69,7 +69,13 @@ func parseHelmVersion(versionStr string) (*semver.Version, error) {
return nil, fmt.Errorf("empty helm version")
}
v, err := chartify.FindSemVerInfo(versionStr)
// Check if version string starts with "v", if not add it
processedVersion := strings.TrimSpace(versionStr)
if !strings.HasPrefix(processedVersion, "v") {
processedVersion = "v" + processedVersion
}
v, err := chartify.FindSemVerInfo(processedVersion)
if err != nil {
return nil, fmt.Errorf("error find helm srmver version '%s': %w", versionStr, err)
@ -116,11 +122,10 @@ func redactedURL(chart string) string {
}
// New for running helm commands
func New(helmBinary string, options HelmExecOptions, logger *zap.SugaredLogger, kubeconfig string, kubeContext string, runner Runner) *execer {
// TODO: proper error handling
func New(helmBinary string, options HelmExecOptions, logger *zap.SugaredLogger, kubeconfig string, kubeContext string, runner Runner) (*execer, error) {
version, err := GetHelmVersion(helmBinary, runner)
if err != nil {
panic(err)
return nil, err
}
if version.Prerelease() != "" {
@ -137,7 +142,7 @@ func New(helmBinary string, options HelmExecOptions, logger *zap.SugaredLogger,
kubeContext: kubeContext,
runner: runner,
decryptedSecrets: make(map[string]*decryptedSecret),
}
}, nil
}
func (helm *execer) SetExtraArgs(args ...string) {

View File

@ -36,16 +36,22 @@ func (mock *mockRunner) Execute(cmd string, args []string, env map[string]string
return mock.output, mock.err
}
func MockExecer(logger *zap.SugaredLogger, kubeconfig, kubeContext string) *execer {
execer := New("helm", HelmExecOptions{}, logger, kubeconfig, kubeContext, &mockRunner{})
return execer
func MockExecer(logger *zap.SugaredLogger, kubeconfig, kubeContext string) (*execer, error) {
execer, err := New("helm", HelmExecOptions{}, logger, kubeconfig, kubeContext, &mockRunner{})
if err != nil {
return nil, err
}
return execer, nil
}
// Test methods
func TestNewHelmExec(t *testing.T) {
buffer := bytes.NewBufferString("something")
helm := MockExecer(NewLogger(buffer, "debug"), "config", "dev")
helm, err := MockExecer(NewLogger(buffer, "debug"), "config", "dev")
if err != nil {
t.Error(err)
}
if helm.kubeContext != "dev" {
t.Error("helmexec.New() - kubeContext")
}
@ -58,7 +64,10 @@ func TestNewHelmExec(t *testing.T) {
}
func Test_SetExtraArgs(t *testing.T) {
helm := MockExecer(NewLogger(os.Stdout, "info"), "config", "dev")
helm, err := MockExecer(NewLogger(os.Stdout, "info"), "config", "dev")
if err != nil {
t.Error(err)
}
helm.SetExtraArgs()
if len(helm.extra) != 0 {
t.Error("helmexec.SetExtraArgs() - passing no arguments should not change extra field")
@ -74,7 +83,10 @@ func Test_SetExtraArgs(t *testing.T) {
}
func Test_SetHelmBinary(t *testing.T) {
helm := MockExecer(NewLogger(os.Stdout, "info"), "config", "dev")
helm, err := MockExecer(NewLogger(os.Stdout, "info"), "config", "dev")
if err != nil {
t.Error(err)
}
if helm.helmBinary != "helm" {
t.Error("helmexec.command - default command is not helm")
}
@ -85,7 +97,10 @@ func Test_SetHelmBinary(t *testing.T) {
}
func Test_SetEnableLiveOutput(t *testing.T) {
helm := MockExecer(NewLogger(os.Stdout, "info"), "config", "dev")
helm, err := MockExecer(NewLogger(os.Stdout, "info"), "config", "dev")
if err != nil {
t.Error(err)
}
if helm.options.EnableLiveOutput {
t.Error("helmexec.options.EnableLiveOutput should not be enabled by default")
}
@ -96,7 +111,10 @@ func Test_SetEnableLiveOutput(t *testing.T) {
}
func Test_SetDisableForceUpdate(t *testing.T) {
helm := MockExecer(NewLogger(os.Stdout, "info"), "config", "dev")
helm, err := MockExecer(NewLogger(os.Stdout, "info"), "config", "dev")
if err != nil {
t.Error(err)
}
if helm.options.DisableForceUpdate {
t.Error("helmexec.options.ForceUpdate should not be enabled by default")
}
@ -155,11 +173,14 @@ exec: helm --kubeconfig config --kube-context dev repo add myRepo https://repo.e
func Test_AddRepo(t *testing.T) {
var buffer bytes.Buffer
logger := NewLogger(&buffer, "debug")
helm := MockExecer(logger, "config", "dev")
helm, err := MockExecer(logger, "config", "dev")
if err != nil {
t.Errorf("unexpected error: %v", err)
}
// Test case with certfile and keyfile
buffer.Reset()
err := helm.AddRepo("myRepo", "https://repo.example.com/", "", "cert.pem", "key.pem", "", "", "", false, false)
err = helm.AddRepo("myRepo", "https://repo.example.com/", "", "cert.pem", "key.pem", "", "", "", false, false)
expected := `Adding repo myRepo https://repo.example.com/
exec: helm --kubeconfig config --kube-context dev repo add myRepo https://repo.example.com/ --cert-file cert.pem --key-file key.pem
`
@ -292,8 +313,11 @@ exec: helm --kubeconfig config --kube-context dev repo add myRepo https://repo.e
func Test_UpdateRepo(t *testing.T) {
var buffer bytes.Buffer
logger := NewLogger(&buffer, "debug")
helm := MockExecer(logger, "config", "dev")
err := helm.UpdateRepo()
helm, err := MockExecer(logger, "config", "dev")
if err != nil {
t.Errorf("unexpected error: %v", err)
}
err = helm.UpdateRepo()
expected := `Updating repo
exec: helm --kubeconfig config --kube-context dev repo update
`
@ -346,8 +370,11 @@ exec: helm --kubeconfig config --kube-context dev registry login repo.example.co
func Test_SyncRelease(t *testing.T) {
var buffer bytes.Buffer
logger := NewLogger(&buffer, "debug")
helm := MockExecer(logger, "config", "dev")
err := helm.SyncRelease(HelmContext{}, "release", "chart", "default", "--timeout 10", "--wait", "--wait-for-jobs")
helm, err := MockExecer(logger, "config", "dev")
if err != nil {
t.Errorf("unexpected error: %v", err)
}
err = helm.SyncRelease(HelmContext{}, "release", "chart", "default", "--timeout 10", "--wait", "--wait-for-jobs")
expected := `Upgrading release=release, chart=chart, namespace=default
exec: helm --kubeconfig config --kube-context dev upgrade --install release chart --timeout 10 --wait --wait-for-jobs --history-max 0
`
@ -386,8 +413,11 @@ exec: helm --kubeconfig config --kube-context dev upgrade --install release http
func Test_UpdateDeps(t *testing.T) {
var buffer bytes.Buffer
logger := NewLogger(&buffer, "debug")
helm := MockExecer(logger, "config", "dev")
err := helm.UpdateDeps("./chart/foo")
helm, err := MockExecer(logger, "config", "dev")
if err != nil {
t.Errorf("unexpected error: %v", err)
}
err = helm.UpdateDeps("./chart/foo")
expected := `Updating dependency ./chart/foo
exec: helm --kubeconfig config --kube-context dev dependency update ./chart/foo
`
@ -416,8 +446,11 @@ func Test_BuildDeps(t *testing.T) {
var buffer bytes.Buffer
logger := NewLogger(&buffer, "debug")
helm3Runner := mockRunner{output: []byte("v3.2.4+ge29ce2a")}
helm := New("helm", HelmExecOptions{}, logger, "config", "dev", &helm3Runner)
err := helm.BuildDeps("foo", "./chart/foo", []string{"--skip-refresh"}...)
helm, err := New("helm", HelmExecOptions{}, logger, "config", "dev", &helm3Runner)
if err != nil {
t.Errorf("unexpected error: %v", err)
}
err = helm.BuildDeps("foo", "./chart/foo", []string{"--skip-refresh"}...)
expected := `Building dependency release=foo, chart=./chart/foo
exec: helm --kubeconfig config --kube-context dev dependency build ./chart/foo --skip-refresh
v3.2.4+ge29ce2a
@ -458,7 +491,10 @@ v3.2.4+ge29ce2a
buffer.Reset()
helm2Runner := mockRunner{output: []byte("Client: v2.16.1+ge13bc94")}
helm = New("helm", HelmExecOptions{}, logger, "config", "dev", &helm2Runner)
helm, err = New("helm", HelmExecOptions{}, logger, "config", "dev", &helm2Runner)
if err != nil {
t.Errorf("unexpected error: %v", err)
}
err = helm.BuildDeps("foo", "./chart/foo")
expected = `Building dependency release=foo, chart=./chart/foo
exec: helm --kubeconfig config --kube-context dev dependency build ./chart/foo
@ -484,14 +520,17 @@ func Test_DecryptSecret(t *testing.T) {
}()
var buffer bytes.Buffer
logger := NewLogger(&buffer, "debug")
helm := MockExecer(logger, "config", "dev")
helm, err := MockExecer(logger, "config", "dev")
if err != nil {
t.Errorf("unexpected error: %v", err)
}
tmpFilePath := "path/to/temp/file"
helm.writeTempFile = func(content []byte) (string, error) {
return tmpFilePath, nil
}
_, err := helm.DecryptSecret(HelmContext{}, "secretName")
_, err = helm.DecryptSecret(HelmContext{}, "secretName")
if err != nil {
t.Errorf("Error: %v", err)
}
@ -533,7 +572,10 @@ func Test_DecryptSecretWithGotmpl(t *testing.T) {
}()
var buffer bytes.Buffer
logger := NewLogger(&buffer, "debug")
helm := MockExecer(logger, "config", "dev")
helm, err := MockExecer(logger, "config", "dev")
if err != nil {
t.Errorf("unexpected error: %v", err)
}
tmpFilePath := "path/to/temp/file"
helm.writeTempFile = func(content []byte) (string, error) {
@ -541,7 +583,7 @@ func Test_DecryptSecretWithGotmpl(t *testing.T) {
}
secretName := "secretName.yaml.gotmpl"
_, err := helm.DecryptSecret(HelmContext{}, secretName)
_, err = helm.DecryptSecret(HelmContext{}, secretName)
if err != nil {
t.Errorf("Error: %v", err)
}
@ -566,8 +608,11 @@ Decrypted %s/secretName.yaml.gotmpl into %s
func Test_DiffRelease(t *testing.T) {
var buffer bytes.Buffer
logger := NewLogger(&buffer, "debug")
helm := MockExecer(logger, "config", "dev")
err := helm.DiffRelease(HelmContext{}, "release", "chart", "default", false, "--timeout 10", "--wait", "--wait-for-jobs")
helm, err := MockExecer(logger, "config", "dev")
if err != nil {
t.Errorf("unexpected error: %v", err)
}
err = helm.DiffRelease(HelmContext{}, "release", "chart", "default", false, "--timeout 10", "--wait", "--wait-for-jobs")
expected := `Comparing release=release, chart=chart, namespace=default
exec: helm --kubeconfig config --kube-context dev diff upgrade --allow-unreleased release chart --timeout 10 --wait --wait-for-jobs
@ -609,8 +654,11 @@ exec: helm --kubeconfig config --kube-context dev diff upgrade --allow-unrelease
func Test_DeleteRelease(t *testing.T) {
var buffer bytes.Buffer
logger := NewLogger(&buffer, "debug")
helm := MockExecer(logger, "config", "dev")
err := helm.DeleteRelease(HelmContext{}, "release")
helm, err := MockExecer(logger, "config", "dev")
if err != nil {
t.Errorf("unexpected error: %v", err)
}
err = helm.DeleteRelease(HelmContext{}, "release")
expected := `Deleting release
exec: helm --kubeconfig config --kube-context dev delete release
`
@ -624,8 +672,11 @@ exec: helm --kubeconfig config --kube-context dev delete release
func Test_DeleteRelease_Flags(t *testing.T) {
var buffer bytes.Buffer
logger := NewLogger(&buffer, "debug")
helm := MockExecer(logger, "config", "dev")
err := helm.DeleteRelease(HelmContext{}, "release", "--purge")
helm, err := MockExecer(logger, "config", "dev")
if err != nil {
t.Errorf("unexpected error: %v", err)
}
err = helm.DeleteRelease(HelmContext{}, "release", "--purge")
expected := `Deleting release
exec: helm --kubeconfig config --kube-context dev delete release --purge
`
@ -640,8 +691,11 @@ exec: helm --kubeconfig config --kube-context dev delete release --purge
func Test_TestRelease(t *testing.T) {
var buffer bytes.Buffer
logger := NewLogger(&buffer, "debug")
helm := MockExecer(logger, "config", "dev")
err := helm.TestRelease(HelmContext{}, "release")
helm, err := MockExecer(logger, "config", "dev")
if err != nil {
t.Errorf("unexpected error: %v", err)
}
err = helm.TestRelease(HelmContext{}, "release")
expected := `Testing release
exec: helm --kubeconfig config --kube-context dev test release
`
@ -655,8 +709,11 @@ exec: helm --kubeconfig config --kube-context dev test release
func Test_TestRelease_Flags(t *testing.T) {
var buffer bytes.Buffer
logger := NewLogger(&buffer, "debug")
helm := MockExecer(logger, "config", "dev")
err := helm.TestRelease(HelmContext{}, "release", "--cleanup", "--timeout", "60")
helm, err := MockExecer(logger, "config", "dev")
if err != nil {
t.Errorf("unexpected error: %v", err)
}
err = helm.TestRelease(HelmContext{}, "release", "--cleanup", "--timeout", "60")
expected := `Testing release
exec: helm --kubeconfig config --kube-context dev test release --cleanup --timeout 60
`
@ -671,8 +728,11 @@ exec: helm --kubeconfig config --kube-context dev test release --cleanup --timeo
func Test_ReleaseStatus(t *testing.T) {
var buffer bytes.Buffer
logger := NewLogger(&buffer, "debug")
helm := MockExecer(logger, "config", "dev")
err := helm.ReleaseStatus(HelmContext{}, "myRelease")
helm, err := MockExecer(logger, "config", "dev")
if err != nil {
t.Errorf("unexpected error: %v", err)
}
err = helm.ReleaseStatus(HelmContext{}, "myRelease")
expected := `Getting status myRelease
exec: helm --kubeconfig config --kube-context dev status myRelease
`
@ -687,9 +747,12 @@ exec: helm --kubeconfig config --kube-context dev status myRelease
func Test_exec(t *testing.T) {
var buffer bytes.Buffer
logger := NewLogger(&buffer, "debug")
helm := MockExecer(logger, "", "")
helm, err := MockExecer(logger, "", "")
if err != nil {
t.Errorf("unexpected error: %v", err)
}
env := map[string]string{}
_, err := helm.exec([]string{"version"}, env, nil)
_, err = helm.exec([]string{"version"}, env, nil)
expected := `exec: helm version
`
if err != nil {
@ -699,14 +762,20 @@ func Test_exec(t *testing.T) {
t.Errorf("helmexec.exec()\nactual = %v\nexpect = %v", buffer.String(), expected)
}
helm = MockExecer(logger, "config", "dev")
helm, err = MockExecer(logger, "config", "dev")
if err != nil {
t.Errorf("unexpected error: %v", err)
}
ret, _ := helm.exec([]string{"diff"}, env, nil)
if len(ret) != 0 {
t.Error("helmexec.exec() - expected empty return value")
}
buffer.Reset()
helm = MockExecer(logger, "config", "dev")
helm, err = MockExecer(logger, "config", "dev")
if err != nil {
t.Errorf("unexpected error: %v", err)
}
_, err = helm.exec([]string{"diff", "release", "chart", "--timeout 10", "--wait", "--wait-for-jobs"}, env, nil)
expected = `exec: helm --kubeconfig config --kube-context dev diff release chart --timeout 10 --wait --wait-for-jobs
`
@ -741,7 +810,10 @@ func Test_exec(t *testing.T) {
}
buffer.Reset()
helm = MockExecer(logger, "", "")
helm, err = MockExecer(logger, "", "")
if err != nil {
t.Errorf("unexpected error: %v", err)
}
helm.SetHelmBinary("overwritten")
_, err = helm.exec([]string{"version"}, env, nil)
expected = `exec: overwritten version
@ -757,8 +829,11 @@ func Test_exec(t *testing.T) {
func Test_Lint(t *testing.T) {
var buffer bytes.Buffer
logger := NewLogger(&buffer, "debug")
helm := MockExecer(logger, "config", "dev")
err := helm.Lint("release", "path/to/chart", "--values", "file.yml")
helm, err := MockExecer(logger, "config", "dev")
if err != nil {
t.Errorf("unexpected error: %v", err)
}
err = helm.Lint("release", "path/to/chart", "--values", "file.yml")
expected := `Linting release=release, chart=path/to/chart
exec: helm --kubeconfig config --kube-context dev lint path/to/chart --values file.yml
`
@ -773,8 +848,11 @@ exec: helm --kubeconfig config --kube-context dev lint path/to/chart --values fi
func Test_Fetch(t *testing.T) {
var buffer bytes.Buffer
logger := NewLogger(&buffer, "debug")
helm := MockExecer(logger, "config", "dev")
err := helm.Fetch("chart", "--version", "1.2.3", "--untar", "--untardir", "/tmp/dir")
helm, err := MockExecer(logger, "config", "dev")
if err != nil {
t.Errorf("unexpected error: %v", err)
}
err = helm.Fetch("chart", "--version", "1.2.3", "--untar", "--untardir", "/tmp/dir")
expected := `Fetching chart
exec: helm --kubeconfig config --kube-context dev fetch chart --version 1.2.3 --untar --untardir /tmp/dir
`
@ -848,8 +926,11 @@ exec: helm --kubeconfig config --kube-context dev pull oci://repo/helm-charts --
tt := tests[i]
t.Run(tt.name, func(t *testing.T) {
buffer.Reset()
helm := New(tt.helmBin, HelmExecOptions{}, logger, "config", "dev", &mockRunner{output: []byte(tt.helmVersion)})
err := helm.ChartPull(tt.chartName, tt.chartPath, tt.chartFlags...)
helm, err := New(tt.helmBin, HelmExecOptions{}, logger, "config", "dev", &mockRunner{output: []byte(tt.helmVersion)})
if err != nil {
t.Errorf("unexpected error: %v", err)
}
err = helm.ChartPull(tt.chartName, tt.chartPath, tt.chartFlags...)
if err != nil {
t.Errorf("unexpected error: %v", err)
}
@ -922,8 +1003,11 @@ func Test_LogLevels(t *testing.T) {
for logLevel, expected := range logLevelTests {
buffer.Reset()
logger := NewLogger(&buffer, logLevel)
helm := MockExecer(logger, "", "")
err := helm.AddRepo("myRepo", "https://repo.example.com/", "", "", "", "example_user", "example_password", "", false, false)
helm, err := MockExecer(logger, "", "")
if err != nil {
t.Errorf("unexpected error: %v", err)
}
err = helm.AddRepo("myRepo", "https://repo.example.com/", "", "", "", "example_user", "example_password", "", false, false)
if err != nil {
t.Errorf("unexpected error: %v", err)
}
@ -951,8 +1035,11 @@ func Test_mergeEnv(t *testing.T) {
func Test_Template(t *testing.T) {
var buffer bytes.Buffer
logger := NewLogger(&buffer, "debug")
helm := MockExecer(logger, "config", "dev")
err := helm.TemplateRelease("release", "path/to/chart", "--values", "file.yml")
helm, err := MockExecer(logger, "config", "dev")
if err != nil {
t.Errorf("unexpected error: %v", err)
}
err = helm.TemplateRelease("release", "path/to/chart", "--values", "file.yml")
expected := `Templating release=release, chart=path/to/chart
exec: helm --kubeconfig config --kube-context dev template release path/to/chart --values file.yml
`
@ -978,13 +1065,19 @@ exec: helm --kubeconfig config --kube-context dev template release https://examp
func Test_IsHelm3(t *testing.T) {
helm2Runner := mockRunner{output: []byte("Client: v2.16.0+ge13bc94\n")}
helm := New("helm", HelmExecOptions{}, NewLogger(os.Stdout, "info"), "", "dev", &helm2Runner)
helm, err := New("helm", HelmExecOptions{}, NewLogger(os.Stdout, "info"), "", "dev", &helm2Runner)
if err != nil {
t.Errorf("unexpected error: %v", err)
}
if helm.IsHelm3() {
t.Error("helmexec.IsHelm3() - Detected Helm 3 with Helm 2 version")
}
helm3Runner := mockRunner{output: []byte("v3.0.0+ge29ce2a\n")}
helm = New("helm", HelmExecOptions{}, NewLogger(os.Stdout, "info"), "", "dev", &helm3Runner)
helm, err = New("helm", HelmExecOptions{}, NewLogger(os.Stdout, "info"), "", "dev", &helm3Runner)
if err != nil {
t.Errorf("unexpected error: %v", err)
}
if !helm.IsHelm3() {
t.Error("helmexec.IsHelm3() - Failed to detect Helm 3")
}
@ -1012,14 +1105,20 @@ func Test_GetPluginVersion(t *testing.T) {
func Test_GetVersion(t *testing.T) {
helm2Runner := mockRunner{output: []byte("Client: v2.16.1+ge13bc94\n")}
helm := New("helm", HelmExecOptions{}, NewLogger(os.Stdout, "info"), "", "dev", &helm2Runner)
helm, err := New("helm", HelmExecOptions{}, NewLogger(os.Stdout, "info"), "", "dev", &helm2Runner)
if err != nil {
t.Errorf("unexpected error: %v", err)
}
ver := helm.GetVersion()
if ver.Major != 2 || ver.Minor != 16 || ver.Patch != 1 {
t.Errorf("helmexec.GetVersion - did not detect correct Helm2 version; it was: %+v", ver)
}
helm3Runner := mockRunner{output: []byte("v3.2.4+ge29ce2a\n")}
helm = New("helm", HelmExecOptions{}, NewLogger(os.Stdout, "info"), "", "dev", &helm3Runner)
helm, err = New("helm", HelmExecOptions{}, NewLogger(os.Stdout, "info"), "", "dev", &helm3Runner)
if err != nil {
t.Errorf("unexpected error: %v", err)
}
ver = helm.GetVersion()
if ver.Major != 3 || ver.Minor != 2 || ver.Patch != 4 {
t.Errorf("helmexec.GetVersion - did not detect correct Helm3 version; it was: %+v", ver)
@ -1028,7 +1127,10 @@ func Test_GetVersion(t *testing.T) {
func Test_IsVersionAtLeast(t *testing.T) {
helm2Runner := mockRunner{output: []byte("Client: v2.16.1+ge13bc94\n")}
helm := New("helm", HelmExecOptions{}, NewLogger(os.Stdout, "info"), "", "dev", &helm2Runner)
helm, err := New("helm", HelmExecOptions{}, NewLogger(os.Stdout, "info"), "", "dev", &helm2Runner)
if err != nil {
t.Errorf("unexpected error: %v", err)
}
if !helm.IsVersionAtLeast("2.1.0") {
t.Error("helmexec.IsVersionAtLeast - 2.16.1 not atleast 2.1")
}
@ -1140,6 +1242,24 @@ func TestParseHelmVersion(t *testing.T) {
want: nil,
wantErr: true,
},
{
name: "version without v prefix",
version: "3.2.4",
want: semver.MustParse("v3.2.4"),
wantErr: false,
},
{
name: "version without v prefix with build info",
version: "3.2.4+ge29ce2a",
want: semver.MustParse("v3.2.4+ge29ce2a"),
wantErr: false,
},
{
name: "version without v prefix with spaces",
version: " 3.2.4 ",
want: semver.MustParse("v3.2.4"),
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {

View File

@ -118,7 +118,7 @@ func Output(ctx context.Context, c *exec.Cmd, stripArgsValuesOnExitError bool, l
exitStatus := waitStatus.ExitStatus()
err = newExitError(c.Path, c.Args, exitStatus, ee, stderr.String(), combined.String(), stripArgsValuesOnExitError)
default:
panic(fmt.Sprintf("unexpected error: %v", err))
err = fmt.Errorf("unexpected error: %v", err)
}
}

View File

@ -5,6 +5,7 @@ import (
"errors"
"fmt"
"io"
"maps"
"net/http"
neturl "net/url"
"os"
@ -14,9 +15,8 @@ import (
"strconv"
"strings"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/s3"
"github.com/aws/aws-sdk-go-v2/config"
"github.com/aws/aws-sdk-go-v2/service/s3"
"github.com/hashicorp/go-getter"
"github.com/hashicorp/go-getter/helper/url"
"go.uber.org/zap"
@ -213,13 +213,16 @@ func (r *Remote) Fetch(path string, cacheDirOpt ...string) (string, error) {
return "", fmt.Errorf("[bug] cacheDirOpt's length: want 0 or 1, got %d", len(cacheDirOpt))
}
query := u.RawQuery
query, _ := neturl.ParseQuery(u.RawQuery)
should_cache := query.Get("cache") != "false"
delete(query, "cache")
var cacheKey string
replacer := strings.NewReplacer(":", "", "//", "_", "/", "_", ".", "_")
dirKey := replacer.Replace(srcDir)
if len(query) > 0 {
q, _ := neturl.ParseQuery(query)
q := maps.Clone(query)
if q.Has("sshkey") {
q.Set("sshkey", "redacted")
}
@ -262,7 +265,7 @@ func (r *Remote) Fetch(path string, cacheDirOpt ...string) (string, error) {
}
}
if !cached {
if !cached || !should_cache {
var getterSrc string
if u.User != "" {
getterSrc = fmt.Sprintf("%s://%s@%s%s", u.Scheme, u.User, u.Host, u.Dir)
@ -271,7 +274,7 @@ func (r *Remote) Fetch(path string, cacheDirOpt ...string) (string, error) {
}
if len(query) > 0 {
getterSrc = strings.Join([]string{getterSrc, query}, "?")
getterSrc = strings.Join([]string{getterSrc, query.Encode()}, "?")
}
r.Logger.Debugf("remote> downloading %s to %s", getterSrc, getterDst)
@ -364,22 +367,22 @@ func (g *S3Getter) Get(wd, src, dst string) error {
return err
}
// Create a new AWS session using the default AWS configuration
sess := session.Must(session.NewSessionWithOptions(session.Options{
SharedConfigState: session.SharedConfigEnable,
Config: aws.Config{
Region: aws.String(region),
},
}))
// Create a new AWS config and S3 client using AWS SDK v2
cfg, err := config.LoadDefaultConfig(context.TODO(),
config.WithRegion(region),
)
if err != nil {
return err
}
// Create an S3 client using the session
s3Client := s3.New(sess)
// Create an S3 client using the config
s3Client := s3.NewFromConfig(cfg)
getObjectInput := &s3.GetObjectInput{
Bucket: &bucket,
Key: &key,
}
resp, err := s3Client.GetObject(getObjectInput)
resp, err := s3Client.GetObject(context.TODO(), getObjectInput)
defer func(Body io.ReadCloser) {
err := Body.Close()
if err != nil {
@ -463,48 +466,47 @@ func (g *S3Getter) S3FileExists(path string) (string, error) {
}
// Region
g.Logger.Debugf("Creating session for determining S3 region %s", path)
sess := session.Must(session.NewSessionWithOptions(session.Options{
SharedConfigState: session.SharedConfigEnable,
}))
g.Logger.Debugf("Creating config for determining S3 region %s", path)
cfg, err := config.LoadDefaultConfig(context.TODO())
if err != nil {
return "", err
}
g.Logger.Debugf("Getting bucket %s location %s", bucket, path)
s3Client := s3.New(sess)
s3Client := s3.NewFromConfig(cfg)
bucketRegion := "us-east-1"
getBucketLocationInput := &s3.GetBucketLocationInput{
Bucket: aws.String(bucket),
Bucket: &bucket,
}
resp, err := s3Client.GetBucketLocation(getBucketLocationInput)
resp, err := s3Client.GetBucketLocation(context.TODO(), getBucketLocationInput)
if err != nil {
return "", fmt.Errorf("Error: Failed to retrieve bucket location: %v\n", err)
return "", fmt.Errorf("failed to retrieve bucket location: %v", err)
}
if resp == nil || resp.LocationConstraint == nil {
if resp == nil || string(resp.LocationConstraint) == "" {
g.Logger.Debugf("Bucket has no location Assuming us-east-1")
} else {
bucketRegion = *resp.LocationConstraint
bucketRegion = string(resp.LocationConstraint)
}
g.Logger.Debugf("Got bucket location %s", bucketRegion)
// File existence
g.Logger.Debugf("Creating new session with region to see if file exists")
regionSession, err := session.NewSessionWithOptions(session.Options{
SharedConfigState: session.SharedConfigEnable,
Config: aws.Config{
Region: aws.String(bucketRegion),
},
})
g.Logger.Debugf("Creating new config with region to see if file exists")
regionCfg, err := config.LoadDefaultConfig(context.TODO(),
config.WithRegion(bucketRegion),
)
if err != nil {
g.Logger.Error(err)
return bucketRegion, err
}
g.Logger.Debugf("Creating new s3 client to check if object exists")
s3Client = s3.New(regionSession)
s3Client = s3.NewFromConfig(regionCfg)
headObjectInput := &s3.HeadObjectInput{
Bucket: &bucket,
Key: &key,
}
g.Logger.Debugf("Fethcing head %s", path)
_, err = s3Client.HeadObject(headObjectInput)
_, err = s3Client.HeadObject(context.TODO(), headObjectInput)
return bucketRegion, err
}

View File

@ -179,7 +179,7 @@ func TestRemote_SShGitHub_WithSshKey(t *testing.T) {
if wd != CacheDir() {
return fmt.Errorf("unexpected wd: %s", wd)
}
if src != "git::ssh://git@github.com/helmfile/helmfiles.git?ref=0.40.0&sshkey=ZWNkc2Etc2hhMi1uaXN0cDI1NiBBQUFBRTJWalpITmhMWE5vWVRJdGJtbHpkSEF5TlRZQUFBQUlibWx6ZEhBeU5UWUFBQUJCQkJTU3dOY2xoVzQ2Vm9VR3dMQ3JscVRHYUdOVWdRVUVEUEptc1ZzdUViL2RBNUcrQk9YMWxGaUVMYU9HQ2F6bS9KQkR2V3Y2Y0ZDQUtVRjVocVJOUjdJPSA=" {
if src != "git::ssh://git@github.com/helmfile/helmfiles.git?ref=0.40.0&sshkey=ZWNkc2Etc2hhMi1uaXN0cDI1NiBBQUFBRTJWalpITmhMWE5vWVRJdGJtbHpkSEF5TlRZQUFBQUlibWx6ZEhBeU5UWUFBQUJCQkJTU3dOY2xoVzQ2Vm9VR3dMQ3JscVRHYUdOVWdRVUVEUEptc1ZzdUViL2RBNUcrQk9YMWxGaUVMYU9HQ2F6bS9KQkR2V3Y2Y0ZDQUtVRjVocVJOUjdJPSA%3D" {
return fmt.Errorf("unexpected src: %s", src)
}
@ -219,6 +219,73 @@ func TestRemote_SShGitHub_WithSshKey(t *testing.T) {
}
}
func TestRemote_SShGitHub_WithDisableCacheKey(t *testing.T) {
cleanfs := map[string]string{
CacheDir(): "",
}
cachefs := map[string]string{
filepath.Join(CacheDir(), "ssh_github_com_helmfile_helmfiles_git.ref=main/releases/kiam.yaml"): "foo: bar",
}
testcases := []struct {
name string
files map[string]string
expectCacheHit bool
}{
{name: "not expectCacheHit", files: cleanfs, expectCacheHit: false},
{name: "forceNoCacheHit", files: cachefs, expectCacheHit: false},
}
for _, tt := range testcases {
t.Run(tt.name, func(t *testing.T) {
testfs := testhelper.NewTestFs(tt.files)
hit := true
get := func(wd, src, dst string) error {
if wd != CacheDir() {
return fmt.Errorf("unexpected wd: %s", wd)
}
if src != "git::ssh://git@github.com/helmfile/helmfiles.git?ref=main" {
return fmt.Errorf("unexpected src: %s", src)
}
hit = false
return nil
}
getter := &testGetter{
get: get,
}
remote := &Remote{
Logger: helmexec.NewLogger(io.Discard, "debug"),
Home: CacheDir(),
Getter: getter,
fs: testfs.ToFileSystem(),
}
url := "git::ssh://git@github.com/helmfile/helmfiles.git@releases/kiam.yaml?ref=main&cache=false"
file, err := remote.Locate(url)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
expectedFile := filepath.Join(CacheDir(), "ssh_github_com_helmfile_helmfiles_git.ref=main/releases/kiam.yaml")
if file != expectedFile {
t.Errorf("unexpected file located: %s vs expected: %s", file, expectedFile)
}
if tt.expectCacheHit && !hit {
t.Errorf("unexpected result: unexpected cache miss")
}
if !tt.expectCacheHit && hit {
t.Errorf("unexpected result: unexpected cache hit")
}
})
}
}
func TestRemote_S3(t *testing.T) {
cleanfs := map[string]string{
CacheDir(): "",

View File

@ -9,15 +9,15 @@ import (
var (
// GoYamlV3 is set to true in order to let Helmfile use
// gopkg.in/yaml.v3 instead of gopkg.in/yaml.v2.
// go.yaml.in/yaml/v3 instead of go.yaml.in/yaml/v2.
// It's false by default in Helmfile v0.x and true in Helmfile v1.x.
GoYamlV3 bool
)
func Info() string {
yamlLib := "gopkg.in/yaml.v2"
yamlLib := "go.yaml.in/yaml/v2"
if GoYamlV3 {
yamlLib = "gopkg.in/yaml.v3"
yamlLib = "go.yaml.in/yaml/v3"
}
return fmt.Sprintf("YAML library = %v", yamlLib)

View File

@ -26,6 +26,8 @@ const (
DefaultHCLFileExtension = ".hcl"
)
var ValidUpdateStrategyValues = []string{UpdateStrategyReinstallIfForbidden}
type StateLoadError struct {
Msg string
Cause error
@ -43,6 +45,14 @@ func (e *UndefinedEnvError) Error() string {
return fmt.Sprintf("environment \"%s\" is not defined", e.Env)
}
type InvalidUpdateStrategyError struct {
UpdateStrategy string
}
func (e *InvalidUpdateStrategyError) Error() string {
return fmt.Sprintf("updateStrategy %q is invalid, valid values are: %s or not set", e.UpdateStrategy, strings.Join(ValidUpdateStrategyValues, ", "))
}
type StateCreator struct {
logger *zap.SugaredLogger
@ -54,7 +64,7 @@ type StateCreator struct {
LoadFile func(inheritedEnv, overrodeEnv *environment.Environment, baseDir, file string, evaluateBases bool) (*HelmState, error)
getHelm func(*HelmState) helmexec.Interface
getHelm func(*HelmState) (helmexec.Interface, error)
overrideHelmBinary string
@ -67,7 +77,7 @@ type StateCreator struct {
lockFile string
}
func NewCreator(logger *zap.SugaredLogger, fs *filesystem.FileSystem, valsRuntime vals.Evaluator, getHelm func(*HelmState) helmexec.Interface, overrideHelmBinary string, overrideKustomizeBinary string, remote *remote.Remote, enableLiveOutput bool, lockFile string) *StateCreator {
func NewCreator(logger *zap.SugaredLogger, fs *filesystem.FileSystem, valsRuntime vals.Evaluator, getHelm func(*HelmState) (helmexec.Interface, error), overrideHelmBinary string, overrideKustomizeBinary string, remote *remote.Remote, enableLiveOutput bool, lockFile string) *StateCreator {
return &StateCreator{
logger: logger,
@ -116,11 +126,19 @@ func (c *StateCreator) Parse(content []byte, baseDir, file string) (*HelmState,
return nil, &StateLoadError{fmt.Sprintf("failed to read %s: reading document at index %d", file, i), err}
}
if err := mergo.Merge(&state, &intermediate, mergo.WithAppendSlice); err != nil {
if err := mergo.Merge(&state, &intermediate, mergo.WithAppendSlice, mergo.WithOverride); err != nil {
return nil, &StateLoadError{fmt.Sprintf("failed to read %s: merging document at index %d", file, i), err}
}
}
state.logger = c.logger
state.valsRuntime = c.valsRuntime
return &state, nil
}
// applyDefaultsAndOverrides applies default binary paths and command-line overrides
func (c *StateCreator) applyDefaultsAndOverrides(state *HelmState) {
if c.overrideHelmBinary != "" && c.overrideHelmBinary != DefaultHelmBinary {
state.DefaultHelmBinary = c.overrideHelmBinary
} else if state.DefaultHelmBinary == "" {
@ -134,11 +152,6 @@ func (c *StateCreator) Parse(content []byte, baseDir, file string) (*HelmState,
// Let `helmfile --kustomize-binary ""` not break this helmfile run
state.DefaultKustomizeBinary = DefaultKustomizeBinary
}
state.logger = c.logger
state.valsRuntime = c.valsRuntime
return &state, nil
}
// LoadEnvValues loads environment values files relative to the `baseDir`
@ -181,6 +194,11 @@ func (c *StateCreator) ParseAndLoad(content []byte, baseDir, file string, envNam
if err != nil {
return nil, err
}
// Apply default binaries and command-line overrides only for the main helmfile
// after loading and merging all bases. This ensures that values from bases are
// properly respected and that later bases/documents can override earlier ones.
c.applyDefaultsAndOverrides(state)
}
state, err = c.LoadEnvValues(state, envName, failOnMissingEnv, envValues, overrode)
@ -216,7 +234,7 @@ func (c *StateCreator) loadBases(envValues, overrodeEnv *environment.Environment
layers = append(layers, st)
for i := 1; i < len(layers); i++ {
if err := mergo.Merge(layers[0], layers[i], mergo.WithAppendSlice); err != nil {
if err := mergo.Merge(layers[0], layers[i], mergo.WithAppendSlice, mergo.WithOverride); err != nil {
return nil, err
}
}
@ -224,6 +242,31 @@ func (c *StateCreator) loadBases(envValues, overrodeEnv *environment.Environment
return layers[0], nil
}
// getEnvMissingFileHandlerConfig returns the first non-nil MissingFileHandlerConfig from the environment spec, state, or default.
func (st *HelmState) getEnvMissingFileHandlerConfig(es EnvironmentSpec) *MissingFileHandlerConfig {
switch {
case es.MissingFileHandlerConfig != nil:
return es.MissingFileHandlerConfig
case st.MissingFileHandlerConfig != nil:
return st.MissingFileHandlerConfig
default:
return nil
}
}
// getEnvMissingFileHandler returns the first non-nil MissingFileHandler from the environment spec, state, or default.
func (st *HelmState) getEnvMissingFileHandler(es EnvironmentSpec) *string {
defaultMissingFileHandler := "Error"
switch {
case es.MissingFileHandler != nil:
return es.MissingFileHandler
case st.MissingFileHandler != nil:
return st.MissingFileHandler
default:
return &defaultMissingFileHandler
}
}
// nolint: unparam
func (c *StateCreator) loadEnvValues(st *HelmState, name string, failOnMissingEnv bool, ctxEnv, overrode *environment.Environment) (*environment.Environment, error) {
secretVals := map[string]any{}
@ -240,7 +283,7 @@ func (c *StateCreator) loadEnvValues(st *HelmState, name string, failOnMissingEn
var envSecretFiles []string
if len(envSpec.Secrets) > 0 {
for _, urlOrPath := range envSpec.Secrets {
resolved, skipped, err := st.storage().resolveFile(envSpec.MissingFileHandler, "environment values", urlOrPath, envSpec.MissingFileHandlerConfig.resolveFileOptions()...)
resolved, skipped, err := st.storage().resolveFile(st.getEnvMissingFileHandler(envSpec), "environment values", urlOrPath, st.getEnvMissingFileHandlerConfig(envSpec).resolveFileOptions()...)
if err != nil {
return nil, err
}
@ -317,7 +360,10 @@ func (c *StateCreator) loadEnvValues(st *HelmState, name string, failOnMissingEn
func (c *StateCreator) scatterGatherEnvSecretFiles(st *HelmState, envSecretFiles []string, envVals map[string]any, keepFileExtensions []string) ([]string, error) {
var errs []error
var decryptedFilesKeeper []string
helm := c.getHelm(st)
helm, err := c.getHelm(st)
if err != nil {
return nil, err
}
inputs := envSecretFiles
inputsSize := len(inputs)

View File

@ -1,6 +1,7 @@
package state
import (
"fmt"
"path/filepath"
"reflect"
"testing"
@ -525,3 +526,205 @@ releaseContext:
t.Errorf("unexpected values: expected=%v, actual=%v", expectedValues, actualValues)
}
}
// TestHelmBinaryInBases tests that helmBinary and kustomizeBinary settings
// from bases are properly merged with later values overriding earlier ones
func TestHelmBinaryInBases(t *testing.T) {
tests := []struct {
name string
files map[string]string
mainFile string
expectedHelmBinary string
expectedKustomizeBinary string
}{
{
name: "helmBinary in second base should be used",
files: map[string]string{
"/path/to/helmfile.yaml": `bases:
- ./bases/env.yaml
---
bases:
- ./bases/repos.yaml
---
bases:
- ./bases/releases.yaml
`,
"/path/to/bases/env.yaml": `environments:
default:
values:
- key: value1
`,
"/path/to/bases/repos.yaml": `repositories:
- name: stable
url: https://charts.helm.sh/stable
helmBinary: /path/to/custom/helm
`,
"/path/to/bases/releases.yaml": `releases:
- name: myapp
chart: stable/nginx
`,
},
mainFile: "/path/to/helmfile.yaml",
expectedHelmBinary: "/path/to/custom/helm",
expectedKustomizeBinary: DefaultKustomizeBinary,
},
{
name: "helmBinary in main file after bases should override",
files: map[string]string{
"/path/to/helmfile.yaml": `bases:
- ./bases/env.yaml
---
bases:
- ./bases/repos.yaml
---
bases:
- ./bases/releases.yaml
helmBinary: /path/to/main/helm
`,
"/path/to/bases/env.yaml": `environments:
default:
values:
- key: value1
`,
"/path/to/bases/repos.yaml": `repositories:
- name: stable
url: https://charts.helm.sh/stable
helmBinary: /path/to/base/helm
`,
"/path/to/bases/releases.yaml": `releases:
- name: myapp
chart: stable/nginx
`,
},
mainFile: "/path/to/helmfile.yaml",
expectedHelmBinary: "/path/to/main/helm",
expectedKustomizeBinary: DefaultKustomizeBinary,
},
{
name: "helmBinary in main file between bases should override earlier bases",
files: map[string]string{
"/path/to/helmfile.yaml": `bases:
- ./bases/env.yaml
---
bases:
- ./bases/repos.yaml
helmBinary: /path/to/middle/helm
---
bases:
- ./bases/releases.yaml
`,
"/path/to/bases/env.yaml": `environments:
default:
values:
- key: value1
`,
"/path/to/bases/repos.yaml": `repositories:
- name: stable
url: https://charts.helm.sh/stable
helmBinary: /path/to/base/helm
`,
"/path/to/bases/releases.yaml": `releases:
- name: myapp
chart: stable/nginx
`,
},
mainFile: "/path/to/helmfile.yaml",
expectedHelmBinary: "/path/to/middle/helm",
expectedKustomizeBinary: DefaultKustomizeBinary,
},
{
name: "kustomizeBinary in base should be used",
files: map[string]string{
"/path/to/helmfile.yaml": `bases:
- ./bases/base.yaml
`,
"/path/to/bases/base.yaml": `kustomizeBinary: /path/to/custom/kustomize
releases:
- name: myapp
chart: mychart
`,
},
mainFile: "/path/to/helmfile.yaml",
expectedHelmBinary: DefaultHelmBinary,
expectedKustomizeBinary: "/path/to/custom/kustomize",
},
{
name: "both helmBinary and kustomizeBinary in different bases",
files: map[string]string{
"/path/to/helmfile.yaml": `bases:
- ./bases/helm.yaml
---
bases:
- ./bases/kustomize.yaml
`,
"/path/to/bases/helm.yaml": `helmBinary: /path/to/custom/helm
`,
"/path/to/bases/kustomize.yaml": `kustomizeBinary: /path/to/custom/kustomize
`,
},
mainFile: "/path/to/helmfile.yaml",
expectedHelmBinary: "/path/to/custom/helm",
expectedKustomizeBinary: "/path/to/custom/kustomize",
},
{
name: "later base overrides earlier base for helmBinary",
files: map[string]string{
"/path/to/helmfile.yaml": `bases:
- ./bases/first.yaml
---
bases:
- ./bases/second.yaml
`,
"/path/to/bases/first.yaml": `helmBinary: /path/to/first/helm
`,
"/path/to/bases/second.yaml": `helmBinary: /path/to/second/helm
`,
},
mainFile: "/path/to/helmfile.yaml",
expectedHelmBinary: "/path/to/second/helm",
expectedKustomizeBinary: DefaultKustomizeBinary,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
testFs := testhelper.NewTestFs(tt.files)
if testFs.Cwd == "" {
testFs.Cwd = "/"
}
r := remote.NewRemote(logger, testFs.Cwd, testFs.ToFileSystem())
creator := NewCreator(logger, testFs.ToFileSystem(), nil, nil, "", "", r, false, "")
// Set up LoadFile for recursive base loading
creator.LoadFile = func(inheritedEnv, overrodeEnv *environment.Environment, baseDir, file string, evaluateBases bool) (*HelmState, error) {
path := filepath.Join(baseDir, file)
content, ok := tt.files[path]
if !ok {
return nil, fmt.Errorf("file not found: %s", path)
}
return creator.ParseAndLoad([]byte(content), filepath.Dir(path), path, DefaultEnv, true, evaluateBases, inheritedEnv, overrodeEnv)
}
yamlContent, ok := tt.files[tt.mainFile]
if !ok {
t.Fatalf("no file named %q registered", tt.mainFile)
}
state, err := creator.ParseAndLoad([]byte(yamlContent), filepath.Dir(tt.mainFile), tt.mainFile, DefaultEnv, true, true, nil, nil)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if state.DefaultHelmBinary != tt.expectedHelmBinary {
t.Errorf("helmBinary mismatch: expected=%s, actual=%s",
tt.expectedHelmBinary, state.DefaultHelmBinary)
}
if state.DefaultKustomizeBinary != tt.expectedKustomizeBinary {
t.Errorf("kustomizeBinary mismatch: expected=%s, actual=%s",
tt.expectedKustomizeBinary, state.DefaultKustomizeBinary)
}
})
}
}

View File

@ -0,0 +1,64 @@
package state
import (
"errors"
"strings"
"sync"
"testing"
"go.uber.org/zap"
"github.com/helmfile/helmfile/pkg/exectest"
"github.com/helmfile/helmfile/pkg/helmexec"
)
func TestIsReleaseInstalled_HandlesConnectionError(t *testing.T) {
logger := zap.NewNop().Sugar()
state := &HelmState{
logger: logger,
}
// Create a custom helm mock that fails on List operations
helm := &CustomFailingHelm{
Helm: &exectest.Helm{
DiffMutex: &sync.Mutex{},
ChartsMutex: &sync.Mutex{},
ReleasesMutex: &sync.Mutex{},
Helm3: true,
},
}
release := ReleaseSpec{
Name: "test-release",
Chart: "test/chart",
Namespace: "default",
}
// This should return an error due to connection failure
_, err := state.isReleaseInstalled(helmexec.HelmContext{}, helm, release)
// Verify that error was propagated
if err == nil {
t.Fatalf("expected isReleaseInstalled to return error when Kubernetes is unreachable, but got no error")
}
if err.Error() == "" {
t.Fatalf("expected isReleaseInstalled to return meaningful error when Kubernetes is unreachable, but got empty error")
}
// Check if the error contains the expected message
expectedMsg := "Kubernetes cluster unreachable"
if err.Error() != expectedMsg && !strings.Contains(err.Error(), "Kubernetes cluster unreachable") {
t.Fatalf("expected error to contain 'Kubernetes cluster unreachable', but got: %v", err.Error())
}
}
// CustomFailingHelm wraps exectest.Helm and overrides List to simulate failures
type CustomFailingHelm struct {
*exectest.Helm
}
func (h *CustomFailingHelm) List(context helmexec.HelmContext, filter string, flags ...string) (string, error) {
return "", errors.New("Kubernetes cluster unreachable: Get \"http://localhost:8080/version\": dial tcp [::1]:8080: connect: connection refused")
}

View File

@ -14,5 +14,5 @@ type EnvironmentSpec struct {
// a message about the missing file at the log-level.
MissingFileHandler *string `yaml:"missingFileHandler,omitempty"`
// MissingFileHandlerConfig is composed of various settings for the MissingFileHandler
MissingFileHandlerConfig MissingFileHandlerConfig `yaml:"missingFileHandlerConfig,omitempty"`
MissingFileHandlerConfig *MissingFileHandlerConfig `yaml:"missingFileHandlerConfig,omitempty"`
}

View File

@ -6,7 +6,6 @@ import (
"path/filepath"
"slices"
"sort"
"strconv"
"strings"
"github.com/helmfile/chartify"
@ -158,31 +157,17 @@ func (st *HelmState) appendWaitForJobsFlags(flags []string, release *ReleaseSpec
return flags
}
func (st *HelmState) appendWaitFlags(flags []string, helm helmexec.Interface, release *ReleaseSpec, ops *SyncOpts) []string {
var hasWait bool
func (st *HelmState) appendWaitFlags(flags []string, release *ReleaseSpec, ops *SyncOpts) []string {
switch {
case release.Wait != nil && *release.Wait:
hasWait = true
flags = append(flags, "--wait")
case ops != nil && ops.Wait:
hasWait = true
flags = append(flags, "--wait")
case release.Wait == nil && st.HelmDefaults.Wait:
hasWait = true
flags = append(flags, "--wait")
}
// see https://github.com/helm/helm/releases/tag/v3.15.0
// https://github.com/helm/helm/commit/fc74964
if hasWait && helm.IsVersionAtLeast("3.15.0") {
switch {
case release.WaitRetries != nil && *release.WaitRetries > 0:
flags = append(flags, "--wait-retries", strconv.Itoa(*release.WaitRetries))
case ops != nil && ops.WaitRetries > 0:
flags = append(flags, "--wait-retries", strconv.Itoa(ops.WaitRetries))
case release.WaitRetries == nil && st.HelmDefaults.WaitRetries > 0:
flags = append(flags, "--wait-retries", strconv.Itoa(st.HelmDefaults.WaitRetries))
}
}
// Note: --wait-retries flag has been removed from Helm and is no longer supported
// WaitRetries configuration is preserved for backward compatibility but ignored
return flags
}
@ -350,7 +335,7 @@ func (st *HelmState) PrepareChartify(helm helmexec.Interface, release *ReleaseSp
jsonPatches := release.JSONPatches
if len(jsonPatches) > 0 {
generatedFiles, err := st.generateTemporaryReleaseValuesFiles(release, jsonPatches, release.MissingFileHandler)
generatedFiles, err := st.generateTemporaryReleaseValuesFiles(release, jsonPatches)
if err != nil {
return nil, clean, err
}
@ -364,7 +349,7 @@ func (st *HelmState) PrepareChartify(helm helmexec.Interface, release *ReleaseSp
strategicMergePatches := release.StrategicMergePatches
if len(strategicMergePatches) > 0 {
generatedFiles, err := st.generateTemporaryReleaseValuesFiles(release, strategicMergePatches, release.MissingFileHandler)
generatedFiles, err := st.generateTemporaryReleaseValuesFiles(release, strategicMergePatches)
if err != nil {
return nil, clean, err
}
@ -378,7 +363,7 @@ func (st *HelmState) PrepareChartify(helm helmexec.Interface, release *ReleaseSp
transformers := release.Transformers
if len(transformers) > 0 {
generatedFiles, err := st.generateTemporaryReleaseValuesFiles(release, transformers, release.MissingFileHandler)
generatedFiles, err := st.generateTemporaryReleaseValuesFiles(release, transformers)
if err != nil {
return nil, clean, err
}

View File

@ -76,7 +76,6 @@ func TestAppendWaitFlags(t *testing.T) {
name string
release *ReleaseSpec
syncOpts *SyncOpts
helm helmexec.Interface
helmSpec HelmSpec
expected []string
}{
@ -85,7 +84,6 @@ func TestAppendWaitFlags(t *testing.T) {
name: "release wait",
release: &ReleaseSpec{Wait: &[]bool{true}[0]},
syncOpts: nil,
helm: testutil.NewVersionHelmExec("3.11.0"),
helmSpec: HelmSpec{},
expected: []string{"--wait"},
},
@ -93,7 +91,6 @@ func TestAppendWaitFlags(t *testing.T) {
name: "cli flags wait",
release: &ReleaseSpec{},
syncOpts: &SyncOpts{Wait: true},
helm: testutil.NewVersionHelmExec("3.11.0"),
helmSpec: HelmSpec{},
expected: []string{"--wait"},
},
@ -101,7 +98,6 @@ func TestAppendWaitFlags(t *testing.T) {
name: "helm defaults wait",
release: &ReleaseSpec{},
syncOpts: nil,
helm: testutil.NewVersionHelmExec("3.11.0"),
helmSpec: HelmSpec{Wait: true},
expected: []string{"--wait"},
},
@ -109,7 +105,6 @@ func TestAppendWaitFlags(t *testing.T) {
name: "release wait false",
release: &ReleaseSpec{Wait: &[]bool{false}[0]},
syncOpts: nil,
helm: testutil.NewVersionHelmExec("3.11.0"),
helmSpec: HelmSpec{Wait: true},
expected: []string{},
},
@ -117,7 +112,6 @@ func TestAppendWaitFlags(t *testing.T) {
name: "cli flags wait false",
release: &ReleaseSpec{},
syncOpts: &SyncOpts{},
helm: testutil.NewVersionHelmExec("3.11.0"),
helmSpec: HelmSpec{Wait: true},
expected: []string{"--wait"},
},
@ -125,66 +119,58 @@ func TestAppendWaitFlags(t *testing.T) {
name: "helm defaults wait false",
release: &ReleaseSpec{},
syncOpts: nil,
helm: testutil.NewVersionHelmExec("3.11.0"),
helmSpec: HelmSpec{Wait: false},
expected: []string{},
},
// --wait-retries
// --wait-retries flag has been removed from Helm
{
name: "release wait and retry unsupported",
release: &ReleaseSpec{Wait: &[]bool{true}[0], WaitRetries: &[]int{1}[0]},
syncOpts: nil,
helm: testutil.NewVersionHelmExec("3.11.0"),
helmSpec: HelmSpec{},
expected: []string{"--wait"},
},
{
name: "release wait and retry supported",
name: "release wait and retry - retries ignored",
release: &ReleaseSpec{Wait: &[]bool{true}[0], WaitRetries: &[]int{1}[0]},
syncOpts: nil,
helm: testutil.NewVersionHelmExec("3.15.0"),
helmSpec: HelmSpec{},
expected: []string{"--wait", "--wait-retries", "1"},
expected: []string{"--wait"},
},
{
name: "no wait retry",
release: &ReleaseSpec{WaitRetries: &[]int{1}[0]},
syncOpts: nil,
helm: testutil.NewVersionHelmExec("3.15.0"),
helmSpec: HelmSpec{},
expected: []string{},
},
{
name: "cli flags wait and retry",
name: "cli flags wait and retry - retries ignored",
release: &ReleaseSpec{},
syncOpts: &SyncOpts{Wait: true, WaitRetries: 2},
helm: testutil.NewVersionHelmExec("3.15.0"),
helmSpec: HelmSpec{},
expected: []string{"--wait", "--wait-retries", "2"},
expected: []string{"--wait"},
},
{
name: "helm defaults wait retry",
name: "helm defaults wait retry - retries ignored",
release: &ReleaseSpec{},
syncOpts: nil,
helm: testutil.NewVersionHelmExec("3.15.0"),
helmSpec: HelmSpec{Wait: true, WaitRetries: 3},
expected: []string{"--wait", "--wait-retries", "3"},
expected: []string{"--wait"},
},
{
name: "release wait default retries",
name: "release wait default retries - retries ignored",
release: &ReleaseSpec{Wait: &[]bool{true}[0]},
syncOpts: nil,
helm: testutil.NewVersionHelmExec("3.15.0"),
helmSpec: HelmSpec{WaitRetries: 4},
expected: []string{"--wait", "--wait-retries", "4"},
expected: []string{"--wait"},
},
{
name: "release retries default wait",
name: "release retries default wait - retries ignored",
release: &ReleaseSpec{WaitRetries: &[]int{5}[0]},
syncOpts: nil,
helm: testutil.NewVersionHelmExec("3.15.0"),
helmSpec: HelmSpec{Wait: true},
expected: []string{"--wait", "--wait-retries", "5"},
expected: []string{"--wait"},
},
}
@ -192,7 +178,7 @@ func TestAppendWaitFlags(t *testing.T) {
t.Run(tt.name, func(t *testing.T) {
st := &HelmState{}
st.HelmDefaults = tt.helmSpec
got := st.appendWaitFlags([]string{}, tt.helm, tt.release, tt.syncOpts)
got := st.appendWaitFlags([]string{}, tt.release, tt.syncOpts)
require.Equalf(t, tt.expected, got, "appendWaitFlags() = %v, want %v", got, tt.expected)
})
}

View File

@ -43,6 +43,9 @@ const (
// This is used by an interim solution to make the urfave/cli command report to the helmfile internal about that the
// --timeout flag is missingl
EmptyTimeout = -1
// Valid enum for updateStrategy values
UpdateStrategyReinstallIfForbidden = "reinstallIfForbidden"
)
// ReleaseSetSpec is release set spec
@ -84,9 +87,9 @@ type ReleaseSetSpec struct {
// If set to "Error", return an error when a subhelmfile points to a
// non-existent path. The default behavior is to print a warning. Note the
// differing default compared to other MissingFileHandlers.
MissingFileHandler string `yaml:"missingFileHandler,omitempty"`
MissingFileHandler *string `yaml:"missingFileHandler,omitempty"`
// MissingFileHandlerConfig is composed of various settings for the MissingFileHandler
MissingFileHandlerConfig MissingFileHandlerConfig `yaml:"missingFileHandlerConfig,omitempty"`
MissingFileHandlerConfig *MissingFileHandlerConfig `yaml:"missingFileHandlerConfig,omitempty"`
LockFile string `yaml:"lockFilePath,omitempty"`
}
@ -163,6 +166,7 @@ type HelmSpec struct {
// Wait, if set to true, will wait until all Pods, PVCs, Services, and minimum number of Pods of a Deployment are in a ready state before marking the release as successful
Wait bool `yaml:"wait"`
// WaitRetries, if set and --wait enabled, will retry any failed check on resource state, except if HTTP status code < 500 is received, subject to the specified number of retries
// DEPRECATED: This field is ignored as the --wait-retries flag was removed from Helm. Preserved for backward compatibility.
WaitRetries int `yaml:"waitRetries"`
// WaitForJobs, if set and --wait enabled, will wait until all Jobs have been completed before marking the release as successful. It will wait for as long as --timeout
WaitForJobs bool `yaml:"waitForJobs"`
@ -264,6 +268,7 @@ type ReleaseSpec struct {
// Wait, if set to true, will wait until all Pods, PVCs, Services, and minimum number of Pods of a Deployment are in a ready state before marking the release as successful
Wait *bool `yaml:"wait,omitempty"`
// WaitRetries, if set and --wait enabled, will retry any failed check on resource state, except if HTTP status code < 500 is received, subject to the specified number of retries
// DEPRECATED: This field is ignored as the --wait-retries flag was removed from Helm. Preserved for backward compatibility.
WaitRetries *int `yaml:"waitRetries,omitempty"`
// WaitForJobs, if set and --wait enabled, will wait until all Jobs have been completed before marking the release as successful. It will wait for as long as --timeout
WaitForJobs *bool `yaml:"waitForJobs,omitempty"`
@ -275,6 +280,8 @@ type ReleaseSpec struct {
Force *bool `yaml:"force,omitempty"`
// Installed, when set to true, `delete --purge` the release
Installed *bool `yaml:"installed,omitempty"`
// UpdateStrategy, when set, indicate the strategy to use to update the release
UpdateStrategy string `yaml:"updateStrategy,omitempty"`
// Atomic, when set to true, restore previous state in case of a failed install/upgrade attempt
Atomic *bool `yaml:"atomic,omitempty"`
// CleanupOnFail, when set to true, the --cleanup-on-fail helm flag is passed to the upgrade command
@ -307,6 +314,10 @@ type ReleaseSpec struct {
// MissingFileHandler is set to either "Error" or "Warn". "Error" instructs helmfile to fail when unable to find a values or secrets file. When "Warn", it prints the file and continues.
// The default value for MissingFileHandler is "Error".
MissingFileHandler *string `yaml:"missingFileHandler,omitempty"`
// MissingFileHandlerConfig is composed of various settings for the MissingFileHandler
MissingFileHandlerConfig *MissingFileHandlerConfig `yaml:"missingFileHandlerConfig,omitempty"`
// Needs is the [KUBECONTEXT/][NS/]NAME representations of releases that this release depends on.
Needs []string `yaml:"needs,omitempty"`
@ -461,6 +472,7 @@ type SetValue struct {
// AffectedReleases hold the list of released that where updated, deleted, or in error
type AffectedReleases struct {
Upgraded []*ReleaseSpec
Reinstalled []*ReleaseSpec
Deleted []*ReleaseSpec
Failed []*ReleaseSpec
DeleteFailed []*ReleaseSpec
@ -803,6 +815,7 @@ type SyncOpts struct {
Wait bool
WaitRetries int
WaitForJobs bool
Timeout int
SyncReleaseLabels bool
ReuseValues bool
ResetValues bool
@ -1030,7 +1043,10 @@ func (st *HelmState) SyncReleases(affectedReleases *AffectedReleases, helm helme
}
m.Unlock()
}
} else if err := helm.SyncRelease(context, release.Name, chart, release.Namespace, flags...); err != nil {
} else if release.UpdateStrategy == UpdateStrategyReinstallIfForbidden {
relErr = st.performSyncOrReinstallOfRelease(affectedReleases, helm, context, release, chart, m, flags...)
} else {
if err := helm.SyncRelease(context, release.Name, chart, release.Namespace, flags...); err != nil {
m.Lock()
affectedReleases.Failed = append(affectedReleases.Failed, release)
m.Unlock()
@ -1046,6 +1062,7 @@ func (st *HelmState) SyncReleases(affectedReleases *AffectedReleases, helm helme
release.installedVersion = installedVersion
}
}
}
if _, err := st.triggerPostsyncEvent(release, relErr, "sync"); err != nil {
if relErr == nil {
@ -1089,6 +1106,77 @@ func (st *HelmState) SyncReleases(affectedReleases *AffectedReleases, helm helme
return nil
}
func (st *HelmState) performSyncOrReinstallOfRelease(affectedReleases *AffectedReleases, helm helmexec.Interface, context helmexec.HelmContext, release *ReleaseSpec, chart string, m *sync.Mutex, flags ...string) *ReleaseError {
if err := helm.SyncRelease(context, release.Name, chart, release.Namespace, flags...); err != nil {
st.logger.Debugf("update strategy - sync failed: %s", err.Error())
// Only fail if a different error than forbidden updates
if !strings.Contains(err.Error(), "Forbidden: updates") {
st.logger.Debugf("update strategy - sync failed not due to Forbidden updates")
m.Lock()
affectedReleases.Failed = append(affectedReleases.Failed, release)
m.Unlock()
return newReleaseFailedError(release, err)
}
} else {
st.logger.Debugf("update strategy - sync success")
m.Lock()
affectedReleases.Upgraded = append(affectedReleases.Upgraded, release)
m.Unlock()
installedVersion, err := st.getDeployedVersion(context, helm, release)
if err != nil { // err is not really impacting so just log it
st.logger.Debugf("update strategy - getting deployed release version failed: %v", err)
} else {
release.installedVersion = installedVersion
}
return nil
}
st.logger.Infof("Failed to sync due to forbidden updates, attempting to reinstall %q allowed by update strategy", release.Name)
installed, err := st.isReleaseInstalled(context, helm, *release)
if err != nil {
return newReleaseFailedError(release, err)
}
if installed {
var args []string
if release.Namespace != "" {
args = append(args, "--namespace", release.Namespace)
}
deleteWaitFlag := true
release.DeleteWait = &deleteWaitFlag
args = st.appendDeleteWaitFlags(args, release)
deletionFlags := st.appendConnectionFlags(args, release)
m.Lock()
if _, err := st.triggerReleaseEvent("preuninstall", nil, release, "sync"); err != nil {
affectedReleases.Failed = append(affectedReleases.Failed, release)
return newReleaseFailedError(release, err)
} else if err := helm.DeleteRelease(context, release.Name, deletionFlags...); err != nil {
affectedReleases.Failed = append(affectedReleases.Failed, release)
return newReleaseFailedError(release, err)
} else if _, err := st.triggerReleaseEvent("postuninstall", nil, release, "sync"); err != nil {
affectedReleases.Failed = append(affectedReleases.Failed, release)
return newReleaseFailedError(release, err)
}
m.Unlock()
}
if err := helm.SyncRelease(context, release.Name, chart, release.Namespace, flags...); err != nil {
m.Lock()
affectedReleases.Failed = append(affectedReleases.Failed, release)
m.Unlock()
return newReleaseFailedError(release, err)
} else {
m.Lock()
affectedReleases.Reinstalled = append(affectedReleases.Reinstalled, release)
m.Unlock()
installedVersion, err := st.getDeployedVersion(context, helm, release)
if err != nil { // err is not really impacting so just log it
st.logger.Debugf("update strategy - getting deployed release version failed: %v", err)
} else {
release.installedVersion = installedVersion
}
}
return nil
}
func (st *HelmState) listReleases(context helmexec.HelmContext, helm helmexec.Interface, release *ReleaseSpec) (string, error) {
flags := st.kubeConnectionFlags(release)
if release.Namespace != "" {
@ -1132,6 +1220,16 @@ func releasesNeedCharts(releases []ReleaseSpec) []ReleaseSpec {
return result
}
func filterReleasesForBuild(releases []ReleaseSpec) []ReleaseSpec {
var filteredReleases []ReleaseSpec
for _, r := range releases {
if len(r.JSONPatches) == 0 && len(r.StrategicMergePatches) == 0 && len(r.Transformers) == 0 {
filteredReleases = append(filteredReleases, r)
}
}
return filteredReleases
}
type ChartPrepareOptions struct {
ForceDownload bool
SkipRepos bool
@ -1184,6 +1282,19 @@ func (st *HelmState) GetRepositoryAndNameFromChartName(chartName string) (*Repos
return nil, chartName
}
var mutexMap sync.Map
// retrieves or creates a sync.Mutex for a given name
func (st *HelmState) getNamedMutex(name string) *sync.Mutex {
mu, ok := mutexMap.Load(name)
if ok {
return mu.(*sync.Mutex)
}
newMu := &sync.Mutex{}
actualMu, _ := mutexMap.LoadOrStore(name, newMu)
return actualMu.(*sync.Mutex)
}
type PrepareChartKey struct {
Namespace, Name, KubeContext string
}
@ -1202,100 +1313,8 @@ type PrepareChartKey struct {
// Otheriwse, if a chart is not a helm chart, it will call "chartify" to turn it into a chart.
//
// If exists, it will also patch resources by json patches, strategic-merge patches, and injectors.
func (st *HelmState) PrepareCharts(helm helmexec.Interface, dir string, concurrency int, helmfileCommand string, opts ChartPrepareOptions) (map[PrepareChartKey]string, []error) {
if !opts.SkipResolve {
updated, err := st.ResolveDeps()
if err != nil {
return nil, []error{err}
}
*st = *updated
}
selected, err := st.GetSelectedReleases(opts.IncludeTransitiveNeeds)
if err != nil {
return nil, []error{err}
}
releases := releasesNeedCharts(selected)
var prepareChartInfoMutex sync.Mutex
prepareChartInfo := make(map[PrepareChartKey]string, len(releases))
errs := []error{}
jobQueue := make(chan *ReleaseSpec, len(releases))
results := make(chan *chartPrepareResult, len(releases))
var builds []*chartPrepareResult
st.scatterGather(
concurrency,
len(releases),
func() {
for i := 0; i < len(releases); i++ {
jobQueue <- &releases[i]
}
close(jobQueue)
},
func(workerIndex int) {
for release := range jobQueue {
if st.OverrideChart != "" {
release.Chart = st.OverrideChart
}
// Call user-defined `prepare` hooks to create/modify local charts to be used by
// the later process.
//
// If it wasn't called here, Helmfile can end up an issue like
// https://github.com/roboll/helmfile/issues/1328
if _, err := st.triggerPrepareEvent(release, helmfileCommand); err != nil {
results <- &chartPrepareResult{err: err}
return
}
chartName := release.Chart
chartPath, err := st.downloadChartWithGoGetter(release)
if err != nil {
results <- &chartPrepareResult{err: fmt.Errorf("release %q: %w", release.Name, err)}
return
}
chartFetchedByGoGetter := chartPath != chartName
if !chartFetchedByGoGetter {
ociChartPath, err := st.getOCIChart(release, dir, helm, opts.OutputDirTemplate)
if err != nil {
results <- &chartPrepareResult{err: fmt.Errorf("release %q: %w", release.Name, err)}
return
}
if ociChartPath != nil {
chartPath = *ociChartPath
}
}
isLocal := st.fs.DirectoryExistsAt(normalizeChart(st.basePath, chartName))
chartification, clean, err := st.PrepareChartify(helm, release, chartPath, workerIndex)
if !opts.SkipCleanup {
// nolint: staticcheck
defer clean()
}
if err != nil {
results <- &chartPrepareResult{err: err}
return
}
var buildDeps bool
skipDepsGlobal := opts.SkipDeps
skipDepsRelease := release.SkipDeps != nil && *release.SkipDeps
skipDepsDefault := release.SkipDeps == nil && st.HelmDefaults.SkipDeps
skipDeps := (!isLocal && !chartFetchedByGoGetter) || skipDepsGlobal || skipDepsRelease || skipDepsDefault
if chartification != nil && helmfileCommand != "pull" {
// processChartification handles the chartification process
func (st *HelmState) processChartification(chartification *Chartify, release *ReleaseSpec, chartPath string, opts ChartPrepareOptions, skipDeps bool) (string, bool, error) {
c := chartify.New(
chartify.HelmBin(st.DefaultHelmBinary),
chartify.KustomizeBin(st.DefaultKustomizeBinary),
@ -1334,41 +1353,135 @@ func (st *HelmState) PrepareCharts(helm helmexec.Interface, dir string, concurre
out, err := c.Chartify(release.Name, chartPath, chartify.WithChartifyOpts(chartifyOpts))
if err != nil {
results <- &chartPrepareResult{err: err}
return
} else {
chartPath = out
return "", false, err
}
chartPath = out
// Skip `helm dep build` and `helm dep up` altogether when the chart is from remote or the dep is
// explicitly skipped.
buildDeps = !skipDeps
} else if normalizedChart := normalizeChart(st.basePath, chartPath); st.fs.DirectoryExistsAt(normalizedChart) {
// At this point, we are sure that chartPath is a local directory containing either:
// - A remote chart fetched by go-getter or
// - A local chart
//
// The chart may have Chart.yaml(and requirements.yaml for Helm 2), and optionally Chart.lock/requirements.lock,
// but no `charts/` directory populated at all, or a subet of chart dependencies are missing in the directory.
//
// In such situation, Helm fails with an error like:
// Error: found in Chart.yaml, but missing in charts/ directory: cert-manager, prometheus, postgresql, gitlab-runner, grafana, redis
//
// (See also https://github.com/roboll/helmfile/issues/1401#issuecomment-670854495)
//
// To avoid it, we need to call a `helm dep build` command on the chart.
// But the command may consistently fail when an outdated Chart.lock exists.
//
// (I've mentioned about such case in https://github.com/roboll/helmfile/pull/1400.)
//
// Trying to run `helm dep build` on the chart regardless of if it's from local or remote is
// problematic, as usually the user would have no way to fix the remote chart on their own.
//
// Given that, we always run `helm dep build` on the chart here, but tolerate any error caused by it
// for a remote chart, so that the user can notice/fix the issue in a local chart while
// a broken remote chart won't completely block their job.
chartPath = normalizedChart
buildDeps := !skipDeps
return chartPath, buildDeps, nil
}
// processLocalChart handles local chart processing
func (st *HelmState) processLocalChart(normalizedChart, dir string, release *ReleaseSpec, helmfileCommand string, opts ChartPrepareOptions, isLocal bool) (string, error) {
chartPath := normalizedChart
if helmfileCommand == "pull" && isLocal {
chartAbsPath := strings.TrimSuffix(filepath.Clean(normalizedChart), "/")
var err error
chartPath, err = generateChartPath(filepath.Base(chartAbsPath), dir, release, opts.OutputDirTemplate)
if err != nil {
return "", err
}
if err := st.fs.CopyDir(normalizedChart, filepath.Clean(chartPath)); err != nil {
return "", err
}
}
return chartPath, nil
}
// forcedDownloadChart handles forced chart downloads
func (st *HelmState) forcedDownloadChart(chartName, dir string, release *ReleaseSpec, helm helmexec.Interface, opts ChartPrepareOptions) (string, error) {
// Check global chart cache first for non-OCI charts
cacheKey := st.getChartCacheKey(release)
if cachedPath, exists := st.checkChartCache(cacheKey); exists && st.fs.DirectoryExistsAt(cachedPath) {
st.logger.Debugf("Chart %s:%s already downloaded, using cached version at %s", chartName, release.Version, cachedPath)
return cachedPath, nil
}
chartPath, err := generateChartPath(chartName, dir, release, opts.OutputDirTemplate)
if err != nil {
return "", err
}
// only fetch chart if it is not already fetched
if _, err := os.Stat(chartPath); os.IsNotExist(err) {
var fetchFlags []string
fetchFlags = st.appendChartVersionFlags(fetchFlags, release)
fetchFlags = append(fetchFlags, "--untar", "--untardir", chartPath)
if err := helm.Fetch(chartName, fetchFlags...); err != nil {
return "", err
}
} else {
st.logger.Infof("\"%s\" has not been downloaded because the output directory \"%s\" already exists", chartName, chartPath)
}
// Set chartPath to be the path containing Chart.yaml, if found
fullChartPath, err := findChartDirectory(chartPath)
if err == nil {
chartPath = filepath.Dir(fullChartPath)
}
// Add to global chart cache
st.addToChartCache(cacheKey, chartPath)
return chartPath, nil
}
// prepareChartForRelease processes a single release and prepares its chart
func (st *HelmState) prepareChartForRelease(release *ReleaseSpec, helm helmexec.Interface, dir string, helmfileCommand string, opts ChartPrepareOptions, workerIndex int) *chartPrepareResult {
if st.OverrideChart != "" {
release.Chart = st.OverrideChart
}
// Call user-defined `prepare` hooks to create/modify local charts to be used by
// the later process.
//
// If it wasn't called here, Helmfile can end up an issue like
// https://github.com/roboll/helmfile/issues/1328
if _, err := st.triggerPrepareEvent(release, helmfileCommand); err != nil {
return &chartPrepareResult{err: err}
}
chartName := release.Chart
chartPath, err := st.downloadChartWithGoGetter(release)
if err != nil {
return &chartPrepareResult{err: fmt.Errorf("release %q: %w", release.Name, err)}
}
chartFetchedByGoGetter := chartPath != chartName
if !chartFetchedByGoGetter {
ociChartPath, err := st.getOCIChart(release, dir, helm, opts)
if err != nil {
return &chartPrepareResult{err: fmt.Errorf("release %q: %w", release.Name, err)}
}
if ociChartPath != nil {
chartPath = *ociChartPath
}
}
isLocal := st.fs.DirectoryExistsAt(normalizeChart(st.basePath, chartName))
chartification, clean, err := st.PrepareChartify(helm, release, chartPath, workerIndex)
if !opts.SkipCleanup {
// nolint: staticcheck
defer clean()
}
if err != nil {
return &chartPrepareResult{err: err}
}
var buildDeps bool
skipDepsGlobal := opts.SkipDeps
skipDepsRelease := release.SkipDeps != nil && *release.SkipDeps
skipDepsDefault := release.SkipDeps == nil && st.HelmDefaults.SkipDeps
skipDeps := (!isLocal && !chartFetchedByGoGetter) || skipDepsGlobal || skipDepsRelease || skipDepsDefault
if chartification != nil && helmfileCommand != "pull" {
chartPath, buildDeps, err = st.processChartification(chartification, release, chartPath, opts, skipDeps)
if err != nil {
return &chartPrepareResult{err: err}
}
} else if normalizedChart := normalizeChart(st.basePath, chartPath); st.fs.DirectoryExistsAt(normalizedChart) {
chartPath, err = st.processLocalChart(normalizedChart, dir, release, helmfileCommand, opts, isLocal)
if err != nil {
return &chartPrepareResult{err: err}
}
buildDeps = !skipDeps
} else if !opts.ForceDownload {
// At this point, we are sure that either:
@ -1383,33 +1496,13 @@ func (st *HelmState) PrepareCharts(helm helmexec.Interface, dir string, concurre
// For helm 2, we `helm fetch` with the version flags and call `helm template`
// WITHOUT the version flags.
} else {
chartPath, err = generateChartPath(chartName, dir, release, opts.OutputDirTemplate)
chartPath, err = st.forcedDownloadChart(chartName, dir, release, helm, opts)
if err != nil {
results <- &chartPrepareResult{err: err}
return
}
// only fetch chart if it is not already fetched
if _, err := os.Stat(chartPath); os.IsNotExist(err) {
var fetchFlags []string
fetchFlags = st.appendChartVersionFlags(fetchFlags, release)
fetchFlags = append(fetchFlags, "--untar", "--untardir", chartPath)
if err := helm.Fetch(chartName, fetchFlags...); err != nil {
results <- &chartPrepareResult{err: err}
return
}
} else {
st.logger.Infof("\"%s\" has not been downloaded because the output directory \"%s\" already exists", chartName, chartPath)
}
// Set chartPath to be the path containing Chart.yaml, if found
fullChartPath, err := findChartDirectory(chartPath)
if err == nil {
chartPath = filepath.Dir(fullChartPath)
return &chartPrepareResult{err: err}
}
}
results <- &chartPrepareResult{
return &chartPrepareResult{
releaseName: release.Name,
chartName: chartName,
releaseNamespace: release.Namespace,
@ -1419,6 +1512,53 @@ func (st *HelmState) PrepareCharts(helm helmexec.Interface, dir string, concurre
skipRefresh: !isLocal || opts.SkipRefresh,
chartFetchedByGoGetter: chartFetchedByGoGetter,
}
}
func (st *HelmState) PrepareCharts(helm helmexec.Interface, dir string, concurrency int, helmfileCommand string, opts ChartPrepareOptions) (map[PrepareChartKey]string, []error) {
if !opts.SkipResolve {
updated, err := st.ResolveDeps()
if err != nil {
return nil, []error{err}
}
*st = *updated
}
selected, err := st.GetSelectedReleases(opts.IncludeTransitiveNeeds)
if err != nil {
return nil, []error{err}
}
releases := releasesNeedCharts(selected)
// For build command, skip releases that require chartify (jsonPatches, etc.)
// as we only need to output state, not actually template the charts
if helmfileCommand == "build" {
releases = filterReleasesForBuild(releases)
}
var prepareChartInfoMutex sync.Mutex
prepareChartInfo := make(map[PrepareChartKey]string, len(releases))
errs := []error{}
jobQueue := make(chan *ReleaseSpec, len(releases))
results := make(chan *chartPrepareResult, len(releases))
var builds []*chartPrepareResult
st.scatterGather(
concurrency,
len(releases),
func() {
for i := 0; i < len(releases); i++ {
jobQueue <- &releases[i]
}
close(jobQueue)
},
func(workerIndex int) {
for release := range jobQueue {
result := st.prepareChartForRelease(release, helm, dir, helmfileCommand, opts, workerIndex)
results <- result
}
},
func() {
@ -1870,7 +2010,7 @@ func (st *HelmState) prepareDiffReleases(helm helmexec.Interface, additionalValu
mu := &sync.RWMutex{}
installedReleases := map[string]bool{}
isInstalled := func(r *ReleaseSpec) bool {
isInstalled := func(r *ReleaseSpec) (bool, error) {
id := ReleaseToID(r)
mu.RLock()
@ -1878,19 +2018,19 @@ func (st *HelmState) prepareDiffReleases(helm helmexec.Interface, additionalValu
mu.RUnlock()
if ok {
return v
return v, nil
}
v, err := st.isReleaseInstalled(st.createHelmContext(r, 0), helm, *r)
if err != nil {
st.logger.Warnf("confirming if the release is already installed or not: %v", err)
} else {
return false, err
}
mu.Lock()
installedReleases[id] = v
mu.Unlock()
}
return v
return v, nil
}
releases := []*ReleaseSpec{}
@ -1935,12 +2075,25 @@ func (st *HelmState) prepareDiffReleases(helm helmexec.Interface, additionalValu
suppressDiff = true
}
if opt.SkipDiffOnInstall && !isInstalled(release) {
if opt.SkipDiffOnInstall {
installed, err := isInstalled(release)
if err != nil {
errs = append(errs, err)
} else if !installed {
results <- diffPrepareResult{release: release, upgradeDueToSkippedDiff: true, suppressDiff: suppressDiff}
continue
}
}
disableValidation := release.DisableValidationOnInstall != nil && *release.DisableValidationOnInstall && !isInstalled(release)
var disableValidation bool
if release.DisableValidationOnInstall != nil && *release.DisableValidationOnInstall {
installed, err := isInstalled(release)
if err != nil {
errs = append(errs, err)
} else {
disableValidation = !installed
}
}
// TODO We need a long-term fix for this :)
// See https://github.com/roboll/helmfile/issues/737
@ -2255,7 +2408,7 @@ func (st *HelmState) TestReleases(helm helmexec.Interface, cleanup bool, timeout
}
if timeout == EmptyTimeout {
flags = append(flags, st.timeoutFlags(&release)...)
flags = append(flags, st.timeoutFlags(&release, nil)...)
} else {
duration := strconv.Itoa(timeout)
duration += "s"
@ -2682,6 +2835,14 @@ func (st *HelmState) appendKeyringFlags(flags []string, release *ReleaseSpec) []
return flags
}
// appendEnableDNSFlags append the helm command-line flag for DNS resolution
func (st *HelmState) appendEnableDNSFlags(flags []string, release *ReleaseSpec) []string {
if release.EnableDNS != nil && *release.EnableDNS || release.EnableDNS == nil && st.HelmDefaults.EnableDNS {
flags = append(flags, "--enable-dns")
}
return flags
}
func (st *HelmState) kubeConnectionFlags(release *ReleaseSpec) []string {
flags := []string{}
if release.KubeContext != "" {
@ -2735,13 +2896,16 @@ func (st *HelmState) needsInsecureSkipTLSVerify(release *ReleaseSpec, repo *Repo
return relSkipTLSVerify || st.HelmDefaults.InsecureSkipTLSVerify || repoSkipTLSVerify
}
func (st *HelmState) timeoutFlags(release *ReleaseSpec) []string {
func (st *HelmState) timeoutFlags(release *ReleaseSpec, ops *SyncOpts) []string {
var flags []string
timeout := st.HelmDefaults.Timeout
if release.Timeout != nil {
timeout = *release.Timeout
}
if ops != nil && ops.Timeout > 0 {
timeout = ops.Timeout
}
if timeout != 0 {
duration := strconv.Itoa(timeout)
duration += "s"
@ -2754,11 +2918,9 @@ func (st *HelmState) timeoutFlags(release *ReleaseSpec) []string {
func (st *HelmState) flagsForUpgrade(helm helmexec.Interface, release *ReleaseSpec, workerIndex int, opt *SyncOpts) ([]string, []string, error) {
var flags []string
flags = st.appendChartVersionFlags(flags, release)
if release.EnableDNS != nil && *release.EnableDNS || release.EnableDNS == nil && st.HelmDefaults.EnableDNS {
flags = append(flags, "--enable-dns")
}
flags = st.appendEnableDNSFlags(flags, release)
flags = st.appendWaitFlags(flags, helm, release, opt)
flags = st.appendWaitFlags(flags, release, opt)
flags = st.appendWaitForJobsFlags(flags, release, opt)
// non-OCI chart should be verified here
@ -2767,7 +2929,7 @@ func (st *HelmState) flagsForUpgrade(helm helmexec.Interface, release *ReleaseSp
flags = st.appendKeyringFlags(flags, release)
}
flags = append(flags, st.timeoutFlags(release)...)
flags = append(flags, st.timeoutFlags(release, opt)...)
if release.Force != nil && *release.Force || release.Force == nil && st.HelmDefaults.Force {
flags = append(flags, "--force")
@ -2881,6 +3043,7 @@ func (st *HelmState) flagsForDiff(helm helmexec.Interface, release *ReleaseSpec,
settings := cli.New()
var flags []string
flags = st.appendChartVersionFlags(flags, release)
flags = st.appendEnableDNSFlags(flags, release)
disableOpenAPIValidation := false
if release.DisableOpenAPIValidation != nil {
@ -3194,7 +3357,7 @@ func (st *HelmState) ExpandedHelmfiles() ([]SubHelmfileSpec, error) {
}
if len(matches) == 0 {
err := fmt.Errorf("no matches for path: %s", hf.Path)
if st.MissingFileHandler == "Error" {
if *st.getMissingFileHandler() == "Error" {
return nil, err
}
st.logger.Warnf("no matches for path: %s", hf.Path)
@ -3241,19 +3404,67 @@ func (st *HelmState) removeFiles(files []string) {
}
}
func (c MissingFileHandlerConfig) resolveFileOptions() []resolveFileOption {
func (c *MissingFileHandlerConfig) resolveFileOptions() []resolveFileOption {
if c == nil {
return []resolveFileOption{
ignoreMissingGitBranch(false),
}
}
return []resolveFileOption{
ignoreMissingGitBranch(c.IgnoreMissingGitBranch),
}
}
func (st *HelmState) generateTemporaryReleaseValuesFiles(release *ReleaseSpec, values []any, missingFileHandler *string) ([]string, error) {
// getReleaseMissingFileHandlerConfig returns the first non-nil MissingFileHandlerConfig in the following order:
// - release.MissingFileHandlerConfig
// - st.MissingFileHandlerConfig
func (st *HelmState) getReleaseMissingFileHandlerConfig(release *ReleaseSpec) *MissingFileHandlerConfig {
switch {
case release.MissingFileHandlerConfig != nil:
return release.MissingFileHandlerConfig
case st.MissingFileHandlerConfig != nil:
return st.MissingFileHandlerConfig
default:
return nil
}
}
// getReleaseMissingFileHandler returns the first non-nil MissingFileHandler in the following order:
// - release.MissingFileHandler
// - st.MissingFileHandler
// - "Error"
func (st *HelmState) getReleaseMissingFileHandler(release *ReleaseSpec) *string {
defaultMissingFileHandler := "Error"
switch {
case release.MissingFileHandler != nil:
return release.MissingFileHandler
case st.MissingFileHandler != nil:
return st.MissingFileHandler
default:
return &defaultMissingFileHandler
}
}
// getMissingFileHandler returns the first non-nil MissingFileHandler in the following order:
// - st.MissingFileHandler
// - "Error"
func (st *HelmState) getMissingFileHandler() *string {
defaultMissingFileHandler := "Error"
switch {
case st.MissingFileHandler != nil:
return st.MissingFileHandler
default:
return &defaultMissingFileHandler
}
}
func (st *HelmState) generateTemporaryReleaseValuesFiles(release *ReleaseSpec, values []any) ([]string, error) {
generatedFiles := []string{}
for _, value := range values {
switch typedValue := value.(type) {
case string:
paths, skip, err := st.storage().resolveFile(missingFileHandler, "values", typedValue, st.MissingFileHandlerConfig.resolveFileOptions()...)
paths, skip, err := st.storage().resolveFile(st.getReleaseMissingFileHandler(release), "values", typedValue, st.getReleaseMissingFileHandlerConfig(release).resolveFileOptions()...)
if err != nil {
return generatedFiles, err
}
@ -3334,7 +3545,7 @@ func (st *HelmState) generateVanillaValuesFiles(release *ReleaseSpec) ([]string,
return nil, fmt.Errorf("Failed to render values in %s for release %s: type %T isn't supported", st.FilePath, release.Name, valuesMapSecretsRendered["values"])
}
generatedFiles, err := st.generateTemporaryReleaseValuesFiles(release, valuesSecretsRendered, release.MissingFileHandler)
generatedFiles, err := st.generateTemporaryReleaseValuesFiles(release, valuesSecretsRendered)
if err != nil {
return nil, err
}
@ -3400,7 +3611,7 @@ func (st *HelmState) generateSecretValuesFiles(helm helmexec.Interface, release
generatedDecryptedFiles = append(generatedDecryptedFiles, valfile)
}
generatedFiles, err := st.generateTemporaryReleaseValuesFiles(release, generatedDecryptedFiles, release.MissingFileHandler)
generatedFiles, err := st.generateTemporaryReleaseValuesFiles(release, generatedDecryptedFiles)
if err != nil {
return nil, err
}
@ -3605,6 +3816,28 @@ func (ar *AffectedReleases) DisplayAffectedReleases(logger *zap.SugaredLogger) {
}
logger.Info(tbl.String())
}
if len(ar.Reinstalled) > 0 {
logger.Info("\nREINSTALLED RELEASES:")
tbl, _ := prettytable.NewTable(prettytable.Column{Header: "NAME"},
prettytable.Column{Header: "NAMESPACE", MinWidth: 6},
prettytable.Column{Header: "CHART", MinWidth: 6},
prettytable.Column{Header: "VERSION", MinWidth: 6},
prettytable.Column{Header: "DURATION", AlignRight: true},
)
tbl.Separator = " "
for _, release := range ar.Reinstalled {
modifiedChart, modErr := hideChartCredentials(release.Chart)
if modErr != nil {
logger.Warn("Could not modify chart credentials, %v", modErr)
continue
}
err := tbl.AddRow(release.Name, release.Namespace, modifiedChart, release.installedVersion, release.duration.Round(time.Second))
if err != nil {
logger.Warn("Could not add row, %v", err)
}
}
logger.Info(tbl.String())
}
if len(ar.Deleted) > 0 {
logger.Info("\nDELETED RELEASES:")
tbl, _ := prettytable.NewTable(prettytable.Column{Header: "NAME"},
@ -3937,7 +4170,48 @@ func (st *HelmState) Reverse() {
}
}
func (st *HelmState) getOCIChart(release *ReleaseSpec, tempDir string, helm helmexec.Interface, outputDirTemplate string) (*string, error) {
// Chart cache for both OCI and non-OCI charts to avoid duplicate downloads
type ChartCacheKey struct {
Chart string
Version string
}
var downloadedCharts = make(map[ChartCacheKey]string) // key -> chart path
var downloadedChartsMutex sync.RWMutex
// Legacy OCI-specific cache (kept for backward compatibility)
var downloadedOCICharts = make(map[string]bool)
var downloadedOCIMutex sync.RWMutex
// getChartCacheKey creates a cache key for a chart and version
func (st *HelmState) getChartCacheKey(release *ReleaseSpec) ChartCacheKey {
version := release.Version
if version == "" {
// Use empty string for latest version
version = ""
}
return ChartCacheKey{
Chart: release.Chart,
Version: version,
}
}
// checkChartCache checks if a chart is already downloaded and returns its path
func (st *HelmState) checkChartCache(key ChartCacheKey) (string, bool) {
downloadedChartsMutex.RLock()
defer downloadedChartsMutex.RUnlock()
path, exists := downloadedCharts[key]
return path, exists
}
// addToChartCache adds a chart to the cache
func (st *HelmState) addToChartCache(key ChartCacheKey, path string) {
downloadedChartsMutex.Lock()
defer downloadedChartsMutex.Unlock()
downloadedCharts[key] = path
}
func (st *HelmState) getOCIChart(release *ReleaseSpec, tempDir string, helm helmexec.Interface, opts ChartPrepareOptions) (*string, error) {
qualifiedChartName, chartName, chartVersion, err := st.getOCIQualifiedChartName(release, helm)
if err != nil {
return nil, err
@ -3947,7 +4221,41 @@ func (st *HelmState) getOCIChart(release *ReleaseSpec, tempDir string, helm helm
return nil, nil
}
chartPath, _ := st.getOCIChartPath(tempDir, release, chartName, chartVersion, outputDirTemplate)
// Check global chart cache first
cacheKey := st.getChartCacheKey(release)
if cachedPath, exists := st.checkChartCache(cacheKey); exists && st.fs.DirectoryExistsAt(cachedPath) {
st.logger.Debugf("OCI chart %s:%s already downloaded, using cached version at %s", chartName, chartVersion, cachedPath)
return &cachedPath, nil
}
if opts.OutputDirTemplate == "" {
tempDir = remote.CacheDir()
}
chartPath, _ := st.getOCIChartPath(tempDir, release, chartName, chartVersion, opts.OutputDirTemplate)
mu := st.getNamedMutex(chartPath)
mu.Lock()
defer mu.Unlock()
_, err = os.Stat(tempDir)
if err != nil {
err = os.MkdirAll(tempDir, 0755)
if err != nil {
return nil, err
}
}
downloadedOCIMutex.RLock()
alreadyDownloadedFlag := downloadedOCICharts[chartPath]
downloadedOCIMutex.RUnlock()
if !opts.SkipDeps && !opts.SkipRefresh && !alreadyDownloadedFlag {
err = os.RemoveAll(chartPath)
if err != nil {
return nil, err
}
}
if st.fs.DirectoryExistsAt(chartPath) {
st.logger.Debugf("chart already exists at %s", chartPath)
@ -3974,8 +4282,15 @@ func (st *HelmState) getOCIChart(release *ReleaseSpec, tempDir string, helm helm
return nil, err
}
downloadedOCIMutex.Lock()
downloadedOCICharts[chartPath] = true
downloadedOCIMutex.Unlock()
chartPath = filepath.Dir(fullChartPath)
// Add to global chart cache
st.addToChartCache(cacheKey, chartPath)
return &chartPath, nil
}
@ -4040,15 +4355,15 @@ func (st *HelmState) getOCIChartPath(tempDir string, release *ReleaseSpec, chart
pathElems := []string{tempDir}
if release.Namespace != "" {
pathElems = append(pathElems, release.Namespace)
}
if release.KubeContext != "" {
pathElems = append(pathElems, release.KubeContext)
}
pathElems = append(pathElems, release.Name, chartName, safeVersionPath(chartVersion))
replacer := strings.NewReplacer(
":", "_",
"//", "_",
".", "_",
"&", "_",
)
qName := strings.Split(replacer.Replace(release.Chart), "/")
pathElems = append(pathElems, qName...)
pathElems = append(pathElems, safeVersionPath(chartVersion))
return filepath.Join(pathElems...), nil
}

View File

@ -170,6 +170,7 @@ func TestHelmState_flagsForUpgrade(t *testing.T) {
version *semver.Version
defaults HelmSpec
release *ReleaseSpec
syncOpts *SyncOpts
want []string
wantErr string
}{
@ -455,6 +456,27 @@ func TestHelmState_flagsForUpgrade(t *testing.T) {
"--namespace", "test-namespace",
},
},
{
name: "timeout-from-cli-flag",
defaults: HelmSpec{
Timeout: 123,
},
release: &ReleaseSpec{
Chart: "test/chart",
Version: "0.1",
Timeout: some(456),
Name: "test-charts",
Namespace: "test-namespace",
},
syncOpts: &SyncOpts{
Timeout: 789,
},
want: []string{
"--version", "0.1",
"--timeout", "789s",
"--namespace", "test-namespace",
},
},
{
name: "atomic",
defaults: HelmSpec{
@ -737,7 +759,7 @@ func TestHelmState_flagsForUpgrade(t *testing.T) {
Version: tt.version,
}
args, _, err := state.flagsForUpgrade(helm, tt.release, 0, nil)
args, _, err := state.flagsForUpgrade(helm, tt.release, 0, tt.syncOpts)
if err != nil && tt.wantErr == "" {
t.Errorf("unexpected error flagsForUpgrade: %v", err)
}
@ -1618,6 +1640,112 @@ func TestHelmState_SyncReleasesAffectedRealeases(t *testing.T) {
}
}
func TestHelmState_SyncReleasesAffectedReleasesWithReinstallIfForbidden(t *testing.T) {
no := false
tests := []struct {
name string
releases []ReleaseSpec
installed []bool
wantAffected exectest.Affected
}{
{
name: "2 new",
releases: []ReleaseSpec{
{
Name: "releaseNameFoo-forbidden",
Chart: "foo",
UpdateStrategy: "reinstallIfForbidden",
},
{
Name: "releaseNameBar-forbidden",
Chart: "foo",
UpdateStrategy: "reinstallIfForbidden",
},
},
wantAffected: exectest.Affected{
Upgraded: []*exectest.Release{
{Name: "releaseNameFoo-forbidden", Flags: []string{}},
{Name: "releaseNameBar-forbidden", Flags: []string{}},
},
Reinstalled: nil,
Deleted: nil,
Failed: nil,
},
},
{
name: "1 removed, 1 new, 1 reinstalled first new",
releases: []ReleaseSpec{
{
Name: "releaseNameFoo-forbidden",
Chart: "foo",
UpdateStrategy: "reinstallIfForbidden",
},
{
Name: "releaseNameBar",
Chart: "foo",
UpdateStrategy: "reinstallIfForbidden",
Installed: &no,
},
{
Name: "releaseNameFoo-forbidden",
Chart: "foo",
UpdateStrategy: "reinstallIfForbidden",
},
},
installed: []bool{true, true, true},
wantAffected: exectest.Affected{
Upgraded: []*exectest.Release{
{Name: "releaseNameFoo-forbidden", Flags: []string{}},
},
Reinstalled: []*exectest.Release{
{Name: "releaseNameFoo-forbidden", Flags: []string{}},
},
Deleted: []*exectest.Release{
{Name: "releaseNameBar", Flags: []string{}},
},
Failed: nil,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
state := &HelmState{
ReleaseSetSpec: ReleaseSetSpec{
Releases: tt.releases,
},
logger: logger,
valsRuntime: valsRuntime,
RenderedValues: map[string]any{},
}
helm := &exectest.Helm{
Lists: map[exectest.ListKey]string{},
}
//simulate the release is already installed
for i, release := range tt.releases {
if tt.installed != nil && tt.installed[i] {
helm.Lists[exectest.ListKey{Filter: "^" + release.Name + "$", Flags: "--uninstalling --deployed --failed --pending"}] = release.Name
}
}
affectedReleases := AffectedReleases{}
if err := state.SyncReleases(&affectedReleases, helm, []string{}, 1); err != nil {
if !testEq(affectedReleases.Failed, tt.wantAffected.Failed) {
t.Errorf("HelmState.SyncReleases() error failed for [%s] = %v, want %v", tt.name, affectedReleases.Failed, tt.wantAffected.Failed)
} //else expected error
}
if !testEq(affectedReleases.Upgraded, tt.wantAffected.Upgraded) {
t.Errorf("HelmState.SyncReleases() upgrade failed for [%s] = %v, want %v", tt.name, affectedReleases.Upgraded, tt.wantAffected.Upgraded)
}
if !testEq(affectedReleases.Reinstalled, tt.wantAffected.Reinstalled) {
t.Errorf("HelmState.SyncReleases() reinstalled failed for [%s] = %v, want %v", tt.name, affectedReleases.Reinstalled, tt.wantAffected.Reinstalled)
}
if !testEq(affectedReleases.Deleted, tt.wantAffected.Deleted) {
t.Errorf("HelmState.SyncReleases() deleted failed for [%s] = %v, want %v", tt.name, affectedReleases.Deleted, tt.wantAffected.Deleted)
}
})
}
}
func testEq(a []*ReleaseSpec, b []*exectest.Release) bool {
// If one is nil, the other must also be nil.
if (a == nil) != (b == nil) {
@ -1836,14 +1964,19 @@ func TestHelmState_DiffReleases(t *testing.T) {
}
func TestHelmState_DiffFlags(t *testing.T) {
enable := true
disable := false
tests := []struct {
name string
defaults HelmSpec
releases []ReleaseSpec
helm *exectest.Helm
wantDiffFlags []string
}{
{
name: "release with api version and kubeversion",
defaults: HelmSpec{},
releases: []ReleaseSpec{
{
Name: "releaseName",
@ -1857,6 +1990,7 @@ func TestHelmState_DiffFlags(t *testing.T) {
},
{
name: "release with kubeversion and plain http which is ignored",
defaults: HelmSpec{},
releases: []ReleaseSpec{
{
Name: "releaseName",
@ -1868,6 +2002,44 @@ func TestHelmState_DiffFlags(t *testing.T) {
helm: &exectest.Helm{},
wantDiffFlags: []string{"--kube-version", "1.21"},
},
{
name: "release with enable-dns",
defaults: HelmSpec{EnableDNS: false},
releases: []ReleaseSpec{
{
Name: "releaseName",
Chart: "foo",
EnableDNS: &enable,
},
},
helm: &exectest.Helm{},
wantDiffFlags: []string{"--enable-dns"},
},
{
name: "release with disable-dns override",
defaults: HelmSpec{EnableDNS: true},
releases: []ReleaseSpec{
{
Name: "releaseName",
Chart: "foo",
EnableDNS: &disable,
},
},
helm: &exectest.Helm{},
wantDiffFlags: nil,
},
{
name: "release with enable-dns from default",
defaults: HelmSpec{EnableDNS: true},
releases: []ReleaseSpec{
{
Name: "releaseName",
Chart: "foo",
},
},
helm: &exectest.Helm{},
wantDiffFlags: []string{"--enable-dns"},
},
}
for i := range tests {
tt := tests[i]
@ -1875,6 +2047,7 @@ func TestHelmState_DiffFlags(t *testing.T) {
state := &HelmState{
ReleaseSetSpec: ReleaseSetSpec{
Releases: tt.releases,
HelmDefaults: tt.defaults,
},
logger: logger,
valsRuntime: valsRuntime,
@ -4571,3 +4744,58 @@ func TestPrepareSyncReleases_ValueControlReleaseOverride(t *testing.T) {
require.Equal(t, tt.flags, r.flags, "Wrong value control flag for release %s", r.release.Name)
}
}
func TestChartCacheKey(t *testing.T) {
st := &HelmState{}
// Test case 1: release with version
release1 := &ReleaseSpec{
Chart: "stable/nginx",
Version: "1.2.3",
}
key1 := st.getChartCacheKey(release1)
expected1 := ChartCacheKey{Chart: "stable/nginx", Version: "1.2.3"}
if key1 != expected1 {
t.Errorf("Expected %+v, got %+v", expected1, key1)
}
// Test case 2: release without version
release2 := &ReleaseSpec{
Chart: "stable/nginx",
}
key2 := st.getChartCacheKey(release2)
expected2 := ChartCacheKey{Chart: "stable/nginx", Version: ""}
if key2 != expected2 {
t.Errorf("Expected %+v, got %+v", expected2, key2)
}
}
func TestChartCache(t *testing.T) {
st := &HelmState{}
// Create a test key
key := ChartCacheKey{Chart: "stable/test", Version: "1.0.0"}
path := "/tmp/test-chart"
// Initially, chart should not be in cache
_, exists := st.checkChartCache(key)
if exists {
t.Error("Chart should not be in cache initially")
}
// Add to cache
st.addToChartCache(key, path)
// Now chart should be in cache
cachedPath, exists := st.checkChartCache(key)
if !exists {
t.Error("Chart should be in cache after adding")
}
if cachedPath != path {
t.Errorf("Expected path %s, got %s", path, cachedPath)
}
}

View File

@ -38,39 +38,39 @@ func TestGenerateID(t *testing.T) {
run(testcase{
subject: "baseline",
release: ReleaseSpec{Name: "foo", Chart: "incubator/raw"},
want: "foo-values-57ff559cd5",
want: "foo-values-67dc97cbcb",
})
run(testcase{
subject: "different bytes content",
release: ReleaseSpec{Name: "foo", Chart: "incubator/raw"},
data: []byte(`{"k":"v"}`),
want: "foo-values-75cd785b8b",
want: "foo-values-75d7c4758c",
})
run(testcase{
subject: "different map content",
release: ReleaseSpec{Name: "foo", Chart: "incubator/raw"},
data: map[string]any{"k": "v"},
want: "foo-values-5b44fdc768",
want: "foo-values-685f8cf685",
})
run(testcase{
subject: "different chart",
release: ReleaseSpec{Name: "foo", Chart: "stable/envoy"},
want: "foo-values-6ddb5db94d",
want: "foo-values-75597d9c57",
})
run(testcase{
subject: "different name",
release: ReleaseSpec{Name: "bar", Chart: "incubator/raw"},
want: "bar-values-79bbb8df74",
want: "bar-values-7b77df65ff",
})
run(testcase{
subject: "specific ns",
release: ReleaseSpec{Name: "foo", Chart: "incubator/raw", Namespace: "myns"},
want: "myns-foo-values-c9457ccfb",
want: "myns-foo-values-85f979545c",
})
for id, n := range ids {

View File

@ -358,11 +358,11 @@ func testFromYaml(t *testing.T, GoYamlV3 bool) {
}
func TestFromYaml(t *testing.T) {
t.Run("with gopkg.in/yaml.v2", func(t *testing.T) {
t.Run("with go.yaml.in/yaml/v2", func(t *testing.T) {
testFromYaml(t, true)
})
t.Run("with gopkg.in/yaml.v3", func(t *testing.T) {
t.Run("with go.yaml.in/yaml/v3", func(t *testing.T) {
testFromYaml(t, false)
})
}

View File

@ -4,8 +4,8 @@ import (
"bytes"
"io"
v2 "gopkg.in/yaml.v2"
v3 "gopkg.in/yaml.v3"
v2 "go.yaml.in/yaml/v2"
v3 "go.yaml.in/yaml/v3"
"github.com/helmfile/helmfile/pkg/runtime"
)

View File

@ -13,9 +13,9 @@ func testYamlMarshal(t *testing.T, GoYamlV3 bool) {
var yamlLibraryName string
if GoYamlV3 {
yamlLibraryName = "gopkg.in/yaml.v3"
yamlLibraryName = "go.yaml.in/yaml/v3"
} else {
yamlLibraryName = "gopkg.in/yaml.v2"
yamlLibraryName = "go.yaml.in/yaml/v2"
}
v := runtime.GoYamlV3
@ -49,8 +49,8 @@ func testYamlMarshal(t *testing.T, GoYamlV3 bool) {
Annotation: "on",
}},
expected: map[string]string{
"gopkg.in/yaml.v2": "name: John\ninfo:\n- age: 20\n address: New York\n annotation: \"on\"\n",
"gopkg.in/yaml.v3": "name: John\ninfo:\n - age: 20\n address: New York\n annotation: \"on\"\n",
"go.yaml.in/yaml/v2": "name: John\ninfo:\n- age: 20\n address: New York\n annotation: \"on\"\n",
"go.yaml.in/yaml/v3": "name: John\ninfo:\n - age: 20\n address: New York\n annotation: \"on\"\n",
},
},
}
@ -63,11 +63,11 @@ func testYamlMarshal(t *testing.T, GoYamlV3 bool) {
}
func TestYamlMarshal(t *testing.T) {
t.Run("with gopkg.in/yaml.v2", func(t *testing.T) {
t.Run("with go.yaml.in/yaml/v2", func(t *testing.T) {
testYamlMarshal(t, true)
})
t.Run("with gopkg.in/yaml.v3", func(t *testing.T) {
t.Run("with go.yaml.in/yaml/v3", func(t *testing.T) {
testYamlMarshal(t, false)
})
}

View File

@ -30,12 +30,6 @@ var (
helmShortVersionRegex = regexp.MustCompile(`v\d+\.\d+\.\d+\+[a-z0-9]+`)
)
type ociChart struct {
name string
version string
digest string
}
type Config struct {
LocalDockerRegistry struct {
Enabled bool `yaml:"enabled"`
@ -58,11 +52,11 @@ func (f fakeInit) Force() bool {
}
func TestHelmfileTemplateWithBuildCommand(t *testing.T) {
t.Run("with gopkg.in/yaml.v3", func(t *testing.T) {
t.Run("with go.yaml.in/yaml/v3", func(t *testing.T) {
testHelmfileTemplateWithBuildCommand(t, true)
})
t.Run("with gopkg.in/yaml.v2", func(t *testing.T) {
t.Run("with go.yaml.in/yaml/v2", func(t *testing.T) {
testHelmfileTemplateWithBuildCommand(t, false)
})
}
@ -166,8 +160,6 @@ func testHelmfileTemplateWithBuildCommand(t *testing.T, GoYamlV3 bool) {
}
})
// ociCharts holds a list of chart name, version and digest distributed by local oci registry.
ociCharts := []ociChart{}
// If localDockerRegistry.enabled is set to `true`,
// run the docker registry v2 and push the test charts to the registry
// so that it can be accessed by helm and helmfile as a oci registry based chart repository.
@ -201,16 +193,9 @@ func testHelmfileTemplateWithBuildCommand(t *testing.T, GoYamlV3 bool) {
if !c.IsDir() {
t.Fatalf("%s is not a directory", c)
}
chartName, chartVersion := execHelmShowChart(t, chartPath)
tgzFile := execHelmPackage(t, chartPath)
chartDigest, err := execHelmPush(t, tgzFile, fmt.Sprintf("oci://localhost:%d/myrepo", hostPort))
_, err := execHelmPush(t, tgzFile, fmt.Sprintf("oci://localhost:%d/myrepo", hostPort))
require.NoError(t, err, "Unable to run helm push to local registry: %v", err)
ociCharts = append(ociCharts, ociChart{
name: chartName,
version: chartVersion,
digest: chartDigest,
})
}
}
@ -221,7 +206,7 @@ func testHelmfileTemplateWithBuildCommand(t *testing.T, GoYamlV3 bool) {
helmfileCacheHome := filepath.Join(tmpDir, "helmfile_cache")
// HELM_CONFIG_HOME contains the registry auth file (registry.json) and the index of all the repos added via helm-repo-add (repositories.yaml).
helmConfigHome := filepath.Join(tmpDir, "helm_config")
t.Logf("Using HELM_CACHE_HOME=%s, HELMFILE_CACHE_HOME=%s, HELM_CONFIG_HOME=%s", helmCacheHome, helmfileCacheHome, helmConfigHome)
t.Logf("Using HELM_CACHE_HOME=%s, HELMFILE_CACHE_HOME=%s, HELM_CONFIG_HOME=%s, WD=%s", helmCacheHome, helmfileCacheHome, helmConfigHome, wd)
inputFile := filepath.Join(testdataDir, name, "input.yaml.gotmpl")
outputFile := ""
@ -230,6 +215,7 @@ func testHelmfileTemplateWithBuildCommand(t *testing.T, GoYamlV3 bool) {
} else {
outputFile = filepath.Join(testdataDir, name, "gopkg.in-yaml.v2-output.yaml")
}
expectedOutputFile := filepath.Join(testdataDir, name, "output.yaml")
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
defer cancel()
@ -262,15 +248,8 @@ func testHelmfileTemplateWithBuildCommand(t *testing.T, GoYamlV3 bool) {
gotStr = chartGitFullPathRegex.ReplaceAllString(gotStr, `chart=$$GoGetterPath`)
// Replace helm version with $HelmVersion
gotStr = helmShortVersionRegex.ReplaceAllString(gotStr, `$$HelmVersion`)
// Replace all occurrences of HELMFILE_CACHE_HOME with /home/runner/.cache/helmfile
// for stable test result
gotStr = strings.ReplaceAll(gotStr, helmfileCacheHome, "/home/runner/.cache/helmfile")
// OCI based helm charts are pulled and exported under temporary directory.
// We are not sure the exact name of the temporary directory generated by helmfile,
// so redact its base directory name with $TMP.
if config.LocalDockerRegistry.Enabled {
var releaseName, chartPath string
sc := bufio.NewScanner(strings.NewReader(gotStr))
for sc.Scan() {
if !strings.HasPrefix(sc.Text(), "Templating ") {
@ -281,28 +260,20 @@ func testHelmfileTemplateWithBuildCommand(t *testing.T, GoYamlV3 bool) {
if len(releaseChartParts) != 2 {
t.Fatal("Found unexpected log output of templating oci based helm chart, want=\"Templating release=<release_name>, chart=<chart_name>\"")
}
releaseNamePart, chartPathPart := releaseChartParts[0], releaseChartParts[1]
releaseName = strings.TrimPrefix(releaseNamePart, "release=")
chartPath = chartPathPart
}
for _, ociChart := range ociCharts {
chartPathWithoutTempDirBase := fmt.Sprintf("/%s/%s/%s/%s", releaseName, ociChart.name, ociChart.version, ociChart.name)
var chartPathBase string
if strings.HasSuffix(chartPath, chartPathWithoutTempDirBase) {
chartPathBase = strings.TrimSuffix(chartPath, chartPathWithoutTempDirBase)
}
if len(chartPathBase) != 0 {
gotStr = strings.ReplaceAll(gotStr, chartPathBase, "chart=$TMP")
}
gotStr = strings.ReplaceAll(gotStr, fmt.Sprintf("Digest: %s", ociChart.digest), "Digest: $DIGEST")
}
}
gotStr = strings.ReplaceAll(gotStr, helmfileCacheHome, "$HELMFILE_CACHE_HOME")
gotStr = strings.ReplaceAll(gotStr, wd, "__workingdir__")
if stat, _ := os.Stat(outputFile); stat != nil {
want, err := os.ReadFile(outputFile)
wantStr := strings.ReplaceAll(string(want), "__workingdir__", wd)
require.NoError(t, err)
require.Equal(t, wantStr, gotStr)
require.Equal(t, string(want), gotStr)
} else if stat, _ := os.Stat(expectedOutputFile); stat != nil {
want, err := os.ReadFile(expectedOutputFile)
require.NoError(t, err)
require.Equal(t, string(want), gotStr)
} else {
// To update the test golden image(output.yaml), just remove it and rerun this test.
// We automatically capture the output to `output.yaml` in the test case directory
@ -325,23 +296,6 @@ func execDocker(t *testing.T, args ...string) {
}
}
func execHelmShowChart(t *testing.T, localChart string) (string, string) {
t.Helper()
name, version := "", ""
out := execHelm(t, "show", "chart", localChart)
sc := bufio.NewScanner(strings.NewReader(out))
for sc.Scan() {
if strings.HasPrefix(sc.Text(), "name:") {
name = strings.TrimPrefix(sc.Text(), "name: ")
}
if strings.HasPrefix(sc.Text(), "version:") {
version = strings.TrimPrefix(sc.Text(), "version: ")
}
}
return name, version
}
func execHelmPackage(t *testing.T, localChart string) string {
t.Helper()

View File

@ -1,5 +1,5 @@
Pulling localhost:5001/myrepo/raw:0.1.0
Templating release=foo, chart=$TMP/foo/raw/0.1.0/raw
Templating release=foo, chart=$HELMFILE_CACHE_HOME/myrepo/raw/0.1.0/raw
---
# Source: raw/templates/resources.yaml
apiVersion: v1

View File

@ -0,0 +1,6 @@
localDockerRegistry:
enabled: true
port: 5001
chartifyTempDir: temp2
helmfileArgs:
- template

View File

@ -0,0 +1,22 @@
releases:
- name: foo
chart: oci://localhost:5001/myrepo/raw
version: 0.1.0
values: &oci_chart_pull_direct
- templates:
- |
apiVersion: v1
kind: ConfigMap
metadata:
name: {{`{{ .Release.Name }}`}}
namespace: {{`{{ .Release.Namespace }}`}}
annotations:
chart-version: {{`{{ .Chart.Version }}`}}
data:
values: {{`{{ .Release.Name }}`}}
- name: bar
chart: oci://localhost:5001/myrepo/raw
version: 0.1.0
namespace: ns2
values: *oci_chart_pull_direct

View File

@ -0,0 +1,27 @@
Pulling localhost:5001/myrepo/raw:0.1.0
Templating release=foo, chart=$HELMFILE_CACHE_HOME/oci__localhost_5001/myrepo/raw/0.1.0/raw
---
# Source: raw/templates/resources.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: foo
namespace: default
annotations:
chart-version: 0.1.0
data:
values: foo
Templating release=bar, chart=$HELMFILE_CACHE_HOME/oci__localhost_5001/myrepo/raw/0.1.0/raw
---
# Source: raw/templates/resources.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: bar
namespace: ns2
annotations:
chart-version: 0.1.0
data:
values: bar

View File

@ -0,0 +1,9 @@
# Templating two versions of the same chart with only one pulling of each version
localDockerRegistry:
enabled: true
port: 5001
chartifyTempDir: temp3
helmfileArgs:
# Prevent releases from racing and randomizing the log
- --concurrency=1
- template

View File

@ -0,0 +1,43 @@
repositories:
- name: myrepo
url: localhost:5001/myrepo
oci: true
releases:
- name: foo
chart: myrepo/raw
version: 0.1.0
values: &oci_chart_pull_once_values
- templates:
- |
apiVersion: v1
kind: ConfigMap
metadata:
name: {{`{{ .Release.Name }}`}}
namespace: {{`{{ .Release.Namespace }}`}}
annotations:
chart-version: {{`{{ .Chart.Version }}`}}
data:
values: {{`{{ .Release.Name }}`}}
- name: bar
chart: myrepo/raw
version: 0.1.0
values: *oci_chart_pull_once_values
- name: release-no-default-ns
chart: myrepo/raw
version: 0.1.0
namespace: no-default
values: *oci_chart_pull_once_values
- name: second-version-of-chart
chart: myrepo/raw
version: 0.0.1
namespace: foobar
values: *oci_chart_pull_once_values
- name: first-release-version-of-chart
chart: myrepo/raw
version: 0.1.0
values: *oci_chart_pull_once_values

View File

@ -0,0 +1,67 @@
Pulling localhost:5001/myrepo/raw:0.1.0
Pulling localhost:5001/myrepo/raw:0.0.1
Templating release=foo, chart=$HELMFILE_CACHE_HOME/myrepo/raw/0.1.0/raw
---
# Source: raw/templates/resources.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: foo
namespace: default
annotations:
chart-version: 0.1.0
data:
values: foo
Templating release=bar, chart=$HELMFILE_CACHE_HOME/myrepo/raw/0.1.0/raw
---
# Source: raw/templates/resources.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: bar
namespace: default
annotations:
chart-version: 0.1.0
data:
values: bar
Templating release=release-no-default-ns, chart=$HELMFILE_CACHE_HOME/myrepo/raw/0.1.0/raw
---
# Source: raw/templates/resources.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: release-no-default-ns
namespace: no-default
annotations:
chart-version: 0.1.0
data:
values: release-no-default-ns
Templating release=second-version-of-chart, chart=$HELMFILE_CACHE_HOME/myrepo/raw/0.0.1/raw
---
# Source: raw/templates/resources.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: second-version-of-chart
namespace: foobar
annotations:
chart-version: 0.0.1
data:
values: second-version-of-chart
Templating release=first-release-version-of-chart, chart=$HELMFILE_CACHE_HOME/myrepo/raw/0.1.0/raw
---
# Source: raw/templates/resources.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: first-release-version-of-chart
namespace: default
annotations:
chart-version: 0.1.0
data:
values: first-release-version-of-chart

View File

@ -0,0 +1,7 @@
# Templating few releases with the same chart\version and only one pulling
localDockerRegistry:
enabled: true
port: 5001
chartifyTempDir: temp3
helmfileArgs:
- template

View File

@ -0,0 +1,23 @@
repositories:
- name: myrepo
url: localhost:5001/myrepo
oci: true
releases:
{{- range $i := until 5 }}
- name: release-{{ $i }}
chart: myrepo/raw
version: 0.1.0
values:
- templates:
- |
apiVersion: v1
kind: ConfigMap
metadata:
name: {{`{{ .Release.Name }}`}}
namespace: {{`{{ .Release.Namespace }}`}}
annotations:
chart-version: {{`{{ .Chart.Version }}`}}
data:
values: {{`{{ .Release.Name }}`}}
{{- end }}

View File

@ -0,0 +1,66 @@
Pulling localhost:5001/myrepo/raw:0.1.0
Templating release=release-0, chart=$HELMFILE_CACHE_HOME/myrepo/raw/0.1.0/raw
---
# Source: raw/templates/resources.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: release-0
namespace: default
annotations:
chart-version: 0.1.0
data:
values: release-0
Templating release=release-1, chart=$HELMFILE_CACHE_HOME/myrepo/raw/0.1.0/raw
---
# Source: raw/templates/resources.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: release-1
namespace: default
annotations:
chart-version: 0.1.0
data:
values: release-1
Templating release=release-2, chart=$HELMFILE_CACHE_HOME/myrepo/raw/0.1.0/raw
---
# Source: raw/templates/resources.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: release-2
namespace: default
annotations:
chart-version: 0.1.0
data:
values: release-2
Templating release=release-3, chart=$HELMFILE_CACHE_HOME/myrepo/raw/0.1.0/raw
---
# Source: raw/templates/resources.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: release-3
namespace: default
annotations:
chart-version: 0.1.0
data:
values: release-3
Templating release=release-4, chart=$HELMFILE_CACHE_HOME/myrepo/raw/0.1.0/raw
---
# Source: raw/templates/resources.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: release-4
namespace: default
annotations:
chart-version: 0.1.0
data:
values: release-4

View File

@ -27,7 +27,7 @@ export HELM_DATA_HOME="${helm_dir}/data"
export HELM_HOME="${HELM_DATA_HOME}"
export HELM_PLUGINS="${HELM_DATA_HOME}/plugins"
export HELM_CONFIG_HOME="${helm_dir}/config"
HELM_DIFF_VERSION="${HELM_DIFF_VERSION:-3.12.3}"
HELM_DIFF_VERSION="${HELM_DIFF_VERSION:-3.12.5}"
HELM_GIT_VERSION="${HELM_GIT_VERSION:-1.3.0}"
HELM_SECRETS_VERSION="${HELM_SECRETS_VERSION:-3.15.0}"
export GNUPGHOME="${PWD}/${dir}/.gnupg"
@ -80,6 +80,7 @@ ${kubectl} create namespace ${test_ns} || fail "Could not create namespace ${tes
# TEST CASES----------------------------------------------------------------------------------------------------------
. ${dir}/test-cases/fetch-forl-local-chart.sh
. ${dir}/test-cases/suppress-output-line-regex.sh
. ${dir}/test-cases/chartify-jsonPatches-and-strategicMergePatches.sh
. ${dir}/test-cases/include-template-func.sh

View File

@ -0,0 +1,15 @@
fetch_forl_local_chart_input_dir="${cases_dir}/fetch-forl-local-chart/input"
fetch_forl_local_chart_tmp=$(mktemp -d)
case_title="fetch for local chart"
test_start "$case_title"
info "Comparing fetch-forl-local-chart diff log #$i"
${helmfile} -f ${fetch_forl_local_chart_input_dir}/helmfile.yaml.gotmpl fetch --output-dir ${fetch_forl_local_chart_tmp} || fail "\"helmfile fetch\" shouldn't fail"
cat ${fetch_forl_local_chart_tmp}/helmfile-tests/local-chart/raw/latest/Chart.yaml || fail "Chart.yaml should exist in the fetched local chart directory"
cat ${fetch_forl_local_chart_tmp}/helmfile-tests/local-chart/raw/latest/templates/resources.yaml || fail "templates/resources.yaml should exist in the fetched local chart directory"
echo code=$?
test_pass "$case_title"

View File

@ -0,0 +1,4 @@
releases:
- name: local-chart
chart: ../../../charts/raw
namespace: local-chart