feat: Dependency locking (#593)
In order to maintain predictable deployments, as developer I want to generate and use "lock files" for all chart versions retrieved from a helmfile. This change solves it by (1)enhancing `helmfile deps` to generate a lock file containing all the direct chart dependencies of each helmfile state file and (2)making other helmfile sub-commands reads the lock file and merge the locked version numbers to the helmfile state file being processed. The lock file is named after the helmfile state file being locked, so that you can have multiple set of the helmfile state file and the lock file pairs in a directory. When `helmfile deps` are not explicitly run before commands like `sync`, all the helmfile behavior should remain as before. Let's say you have `helmfile.1.yaml`: ``` repositories: - name: stable url: https://kubernetes-charts.storage.googleapis.com releases: - name: envoy chart: stable/envoy - name: envoy2 chart: stable/envoy ``` `helmfile deps` generates `helmfile.1.lock` that looks like: ``` dependencies: - name: envoy repository: https://kubernetes-charts.storage.googleapis.com version: 1.5.0 digest: sha256:e43b05c8528ea8ef1560f4980a519719ad2a634658abde0a98daefdb83a104e9 generated: 2019-05-14T16:45:37.78205+09:00 ``` Under the hood, `helmfile deps` creates a temporary local helm chart with a dummy `Chart.yaml` and `requirements.yaml` deduced from the `helmfile.yaml` content, then runs `helm dependency update` to produce od update the corresponding `requirements.lock` file. `helmfile` then renames it to match the name of the targeted helmfile state file and moves it, so that it becomes adjacent to each `helmfile.yaml`. Other `helmfile` commands like `sync`, `diiff`, `apply`, `lint` read chart version numbers from the lock file. Resolves #483
This commit is contained in:
parent
9eec72b318
commit
c9a43ad9cb
15
README.md
15
README.md
|
|
@ -261,6 +261,20 @@ dependencies of any referenced local charts.
|
|||
|
||||
For Helm 2.9+ you can use a username and password to authenticate to a remote repository.
|
||||
|
||||
### deps
|
||||
|
||||
The `helmfile deps` sub-command locks your helmfile state and local charts dependencies.
|
||||
|
||||
It basically runs `helm dependency update` on your helmfile state file and all the referenced local charts, so that you get a "lock" file per each helmfile state or local chart.
|
||||
|
||||
All the other `helmfile` sub-commands like `sync` use chart versions recorded in the lock files, so that e.g. untested chart versions won't suddenly get deployed to the production environment.
|
||||
|
||||
For example, the lock file for a helmfile state file named `helmfile.1.yaml` will be `helmfile.1.lock`. The lock file for a local chart would be `requirements.lock`, which is the same as `helm`.
|
||||
|
||||
It is recommended to version-control all the lock files, so that they can be used in the production deployment pipeline for extra reproducibility.
|
||||
|
||||
To bring in chart updates systematically, it would also be a good idea to run `helmfile deps` regularly, test it, and then update the lock files in the version-control system.
|
||||
|
||||
### diff
|
||||
|
||||
The `helmfile diff` sub-command executes the [helm-diff](https://github.com/databus23/helm-diff) plugin across all of
|
||||
|
|
@ -292,6 +306,7 @@ The `helmfile delete` sub-command deletes all the releases defined in the manife
|
|||
|
||||
Note that `delete` doesn't purge releases. So `helmfile delete && helmfile sync` results in sync failed due to that releases names are not deleted but preserved for future references. If you really want to remove releases for reuse, add `--purge` flag to run it like `helmfile delete --purge`.
|
||||
|
||||
|
||||
### secrets
|
||||
|
||||
The `secrets` parameter in a `helmfile.yaml` causes the [helm-secrets](https://github.com/futuresimple/helm-secrets) plugin to be executed to decrypt the file.
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import (
|
|||
"github.com/urfave/cli"
|
||||
)
|
||||
|
||||
func Deps(a *app.App) cli.Command {
|
||||
func Deps() cli.Command {
|
||||
return cli.Command{
|
||||
Name: "deps",
|
||||
Usage: "update charts based on the contents of requirements.yaml",
|
||||
|
|
|
|||
|
|
@ -20,3 +20,7 @@ type Interface interface {
|
|||
List(context HelmContext, filter string, flags ...string) (string, error)
|
||||
DecryptSecret(context HelmContext, name string, flags ...string) (string, error)
|
||||
}
|
||||
|
||||
type DependencyUpdater interface {
|
||||
UpdateDeps(chart string) error
|
||||
}
|
||||
|
|
|
|||
11
main.go
11
main.go
|
|
@ -90,6 +90,7 @@ func main() {
|
|||
|
||||
cliApp.Before = configureLogging
|
||||
cliApp.Commands = []cli.Command{
|
||||
cmd.Deps(),
|
||||
{
|
||||
Name: "repos",
|
||||
Usage: "sync repositories from state file (helm repo add && helm repo update)",
|
||||
|
|
@ -184,7 +185,7 @@ func main() {
|
|||
return errs
|
||||
}
|
||||
}
|
||||
if errs := state.PrepareRelease(helm, "diff"); errs != nil && len(errs) > 0 {
|
||||
if errs := state.PrepareReleases(helm, "diff"); errs != nil && len(errs) > 0 {
|
||||
return errs
|
||||
}
|
||||
|
||||
|
|
@ -226,7 +227,7 @@ func main() {
|
|||
return errs
|
||||
}
|
||||
}
|
||||
if errs := state.PrepareRelease(helm, "template"); errs != nil && len(errs) > 0 {
|
||||
if errs := state.PrepareReleases(helm, "template"); errs != nil && len(errs) > 0 {
|
||||
return errs
|
||||
}
|
||||
return executeTemplateCommand(c, state, helm)
|
||||
|
|
@ -260,7 +261,7 @@ func main() {
|
|||
if errs := ctx.SyncReposOnce(state, helm); errs != nil && len(errs) > 0 {
|
||||
return errs
|
||||
}
|
||||
if errs := state.PrepareRelease(helm, "lint"); errs != nil && len(errs) > 0 {
|
||||
if errs := state.PrepareReleases(helm, "lint"); errs != nil && len(errs) > 0 {
|
||||
return errs
|
||||
}
|
||||
return state.LintReleases(helm, values, args, workers)
|
||||
|
|
@ -301,7 +302,7 @@ func main() {
|
|||
return errs
|
||||
}
|
||||
}
|
||||
if errs := st.PrepareRelease(helm, "sync"); errs != nil && len(errs) > 0 {
|
||||
if errs := st.PrepareReleases(helm, "sync"); errs != nil && len(errs) > 0 {
|
||||
return errs
|
||||
}
|
||||
return executeSyncCommand(c, &affectedReleases, st, helm)
|
||||
|
|
@ -348,7 +349,7 @@ func main() {
|
|||
return errs
|
||||
}
|
||||
}
|
||||
if errs := st.PrepareRelease(helm, "apply"); errs != nil && len(errs) > 0 {
|
||||
if errs := st.PrepareReleases(helm, "apply"); errs != nil && len(errs) > 0 {
|
||||
return errs
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,365 @@
|
|||
package state
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/roboll/helmfile/helmexec"
|
||||
"go.uber.org/zap"
|
||||
"gopkg.in/yaml.v2"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type ChartMeta struct {
|
||||
Name string `yaml:"name"`
|
||||
}
|
||||
|
||||
type unresolvedChartDependency struct {
|
||||
// ChartName identifies the dependant chart. In Helmfile, ChartName for `chart: stable/envoy` would be just `envoy`.
|
||||
// It can't be collided with other charts referenced in the same helmfile spec.
|
||||
// That is, collocating `chart: incubator/foo` and `chart: stable/foo` isn't allowed. Name them differently for a work-around.
|
||||
ChartName string `yaml:"name"`
|
||||
// Repository contains the URL for the helm chart repository that hosts the chart identified by ChartName
|
||||
Repository string `yaml:"repository"`
|
||||
// VersionConstraint is the version constraint of the dependent chart. "*" means the latest version.
|
||||
VersionConstraint string `yaml:"version"`
|
||||
}
|
||||
|
||||
type ResolvedChartDependency struct {
|
||||
// ChartName identifies the dependant chart. In Helmfile, ChartName for `chart: stable/envoy` would be just `envoy`.
|
||||
// It can't be collided with other charts referenced in the same helmfile spec.
|
||||
// That is, collocating `chart: incubator/foo` and `chart: stable/foo` isn't allowed. Name them differently for a work-around.
|
||||
ChartName string `yaml:"name"`
|
||||
// Repository contains the URL for the helm chart repository that hosts the chart identified by ChartName
|
||||
Repository string `yaml:"repository"`
|
||||
// Version is the version number of the dependent chart.
|
||||
// In the context of helmfile this can be omitted. When omitted, it is considered `*` which results helm/helmfile fetching the latest version.
|
||||
Version string `yaml:"version"`
|
||||
}
|
||||
|
||||
// StatePackage is for packaging your helmfile state file along with its dependencies.
|
||||
// The only type of dependency currently supported is `chart`.
|
||||
// It is transient and generated on demand while resolving dependencies, and automatically removed afterwards.
|
||||
type StatePackage struct {
|
||||
// name is the name of the package.
|
||||
// Usually this is the "basename" of the helmfile state file, e.g. `helmfile.2` when the state file is named `helmfile.2.yaml`, `helmfille.2.gotmpl`, or `helmfile.2.yaml.gotmpl`.
|
||||
name string
|
||||
|
||||
chartDependencies map[string]unresolvedChartDependency
|
||||
}
|
||||
|
||||
type UnresolvedDependencies struct {
|
||||
deps map[string]unresolvedChartDependency
|
||||
}
|
||||
|
||||
type ChartRequirements struct {
|
||||
UnresolvedDependencies []unresolvedChartDependency `yaml:"dependencies"`
|
||||
}
|
||||
|
||||
type ChartLockedRequirements struct {
|
||||
ResolvedDependencies []ResolvedChartDependency `yaml:"dependencies"`
|
||||
}
|
||||
|
||||
func (d *UnresolvedDependencies) Add(chart, url, versionConstraint string) error {
|
||||
dep := unresolvedChartDependency{
|
||||
ChartName: chart,
|
||||
Repository: url,
|
||||
VersionConstraint: versionConstraint,
|
||||
}
|
||||
return d.add(dep)
|
||||
}
|
||||
|
||||
func (d *UnresolvedDependencies) add(dep unresolvedChartDependency) error {
|
||||
existing, exists := d.deps[dep.ChartName]
|
||||
if exists && (existing.Repository != dep.Repository || existing.VersionConstraint != dep.VersionConstraint) {
|
||||
return fmt.Errorf("duplicate chart dependency \"%s\". you can't have two or more charts with the same name but with different urls or versions: existing=%v, new=%v", dep.ChartName, existing, dep)
|
||||
}
|
||||
d.deps[dep.ChartName] = dep
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *UnresolvedDependencies) ToChartRequirements() *ChartRequirements {
|
||||
deps := []unresolvedChartDependency{}
|
||||
|
||||
for _, d := range d.deps {
|
||||
if d.VersionConstraint == "" {
|
||||
d.VersionConstraint = "*"
|
||||
}
|
||||
deps = append(deps, d)
|
||||
}
|
||||
|
||||
return &ChartRequirements{UnresolvedDependencies: deps}
|
||||
}
|
||||
|
||||
type ResolvedDependencies struct {
|
||||
deps map[string]ResolvedChartDependency
|
||||
}
|
||||
|
||||
func (d *ResolvedDependencies) add(dep ResolvedChartDependency) error {
|
||||
_, exists := d.deps[dep.ChartName]
|
||||
if exists {
|
||||
return fmt.Errorf("duplicate chart dependency \"%s\"", dep.ChartName)
|
||||
}
|
||||
d.deps[dep.ChartName] = dep
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *ResolvedDependencies) Get(chart string) (string, error) {
|
||||
dep, exists := d.deps[chart]
|
||||
if !exists {
|
||||
return "", fmt.Errorf("no resolved dependency found for \"%s\"", chart)
|
||||
}
|
||||
return dep.Version, nil
|
||||
}
|
||||
|
||||
func resolveRemoteChart(repoAndChart string) (string, string, bool) {
|
||||
parts := strings.Split(repoAndChart, "/")
|
||||
if isLocalChart(repoAndChart) {
|
||||
return "", "", false
|
||||
}
|
||||
if len(parts) != 2 {
|
||||
panic(fmt.Sprintf("unsupported format of chart name: %s", repoAndChart))
|
||||
}
|
||||
|
||||
repo := parts[0]
|
||||
chart := parts[1]
|
||||
|
||||
return repo, chart, true
|
||||
}
|
||||
|
||||
func (st *HelmState) mergeLockedDependencies() (*HelmState, error) {
|
||||
filename, unresolved, err := getUnresolvedDependenciess(st)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(unresolved.deps) == 0 {
|
||||
return st, nil
|
||||
}
|
||||
|
||||
depMan := NewChartDependencyManager(filename, st.logger)
|
||||
|
||||
return resolveDependencies(st, depMan, unresolved)
|
||||
}
|
||||
|
||||
func resolveDependencies(st *HelmState, depMan *chartDependencyManager, unresolved *UnresolvedDependencies) (*HelmState, error) {
|
||||
resolved, lockfileExists, err := depMan.Resolve(unresolved)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to resolve %d deps: %v", len(unresolved.deps), err)
|
||||
}
|
||||
if !lockfileExists {
|
||||
return st, nil
|
||||
}
|
||||
|
||||
repoToURL := map[string]string{}
|
||||
|
||||
for _, r := range st.Repositories {
|
||||
repoToURL[r.Name] = r.URL
|
||||
}
|
||||
|
||||
updated := *st
|
||||
for i, r := range updated.Releases {
|
||||
repo, chart, ok := resolveRemoteChart(r.Chart)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
_, ok = repoToURL[repo]
|
||||
// Skip this chart from dependency management, as there's no matching `repository` in the helmfile state,
|
||||
// which may imply that this is a local chart within a directory, like `charts/myapp`
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
ver, err := resolved.Get(chart)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
updated.Releases[i].Version = ver
|
||||
}
|
||||
|
||||
return &updated, nil
|
||||
}
|
||||
|
||||
func (st *HelmState) updateDependenciesInTempDir(shell helmexec.DependencyUpdater, tempDir func(string, string) (string, error)) (*HelmState, error) {
|
||||
filename, unresolved, err := getUnresolvedDependenciess(st)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(unresolved.deps) == 0 {
|
||||
return st, nil
|
||||
}
|
||||
|
||||
d, err := tempDir("", "")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to create dir: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(d)
|
||||
|
||||
return updateDependencies(st, shell, unresolved, filename, d)
|
||||
}
|
||||
|
||||
func getUnresolvedDependenciess(st *HelmState) (string, *UnresolvedDependencies, error) {
|
||||
repoToURL := map[string]string{}
|
||||
|
||||
for _, r := range st.Repositories {
|
||||
repoToURL[r.Name] = r.URL
|
||||
}
|
||||
|
||||
unresolved := &UnresolvedDependencies{deps: map[string]unresolvedChartDependency{}}
|
||||
//if err := unresolved.Add("stable/envoy", "https://kubernetes-charts.storage.googleapis.com", ""); err != nil {
|
||||
// panic(err)
|
||||
//}
|
||||
|
||||
for _, r := range st.Releases {
|
||||
repo, chart, ok := resolveRemoteChart(r.Chart)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
url, ok := repoToURL[repo]
|
||||
// Skip this chart from dependency management, as there's no matching `repository` in the helmfile state,
|
||||
// which may imply that this is a local chart within a directory, like `charts/myapp`
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
if err := unresolved.Add(chart, url, r.Version); err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
}
|
||||
|
||||
filename := filepath.Base(st.FilePath)
|
||||
filename = strings.TrimSuffix(filename, ".gotmpl")
|
||||
filename = strings.TrimSuffix(filename, ".yaml")
|
||||
filename = strings.TrimSuffix(filename, ".yml")
|
||||
|
||||
return filename, unresolved, nil
|
||||
}
|
||||
|
||||
func updateDependencies(st *HelmState, shell helmexec.DependencyUpdater, unresolved *UnresolvedDependencies, filename, wd string) (*HelmState, error) {
|
||||
depMan := NewChartDependencyManager(filename, st.logger)
|
||||
|
||||
_, err := depMan.Update(shell, wd, unresolved)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to resolve %d deps: %v", len(unresolved.deps), err)
|
||||
}
|
||||
|
||||
return resolveDependencies(st, depMan, unresolved)
|
||||
}
|
||||
|
||||
type chartDependencyManager struct {
|
||||
Name string
|
||||
|
||||
logger *zap.SugaredLogger
|
||||
|
||||
readFile func(string) ([]byte, error)
|
||||
writeFile func(string, []byte, os.FileMode) error
|
||||
}
|
||||
|
||||
func NewChartDependencyManager(name string, logger *zap.SugaredLogger) *chartDependencyManager {
|
||||
return &chartDependencyManager{
|
||||
Name: name,
|
||||
readFile: ioutil.ReadFile,
|
||||
writeFile: ioutil.WriteFile,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
func (m *chartDependencyManager) lockFileName() string {
|
||||
return fmt.Sprintf("%s.lock", m.Name)
|
||||
}
|
||||
|
||||
func (m *chartDependencyManager) Update(shell helmexec.DependencyUpdater, wd string, unresolved *UnresolvedDependencies) (*ResolvedDependencies, error) {
|
||||
// Generate `Chart.yaml` of the temporary local chart
|
||||
if err := m.writeBytes(filepath.Join(wd, "Chart.yaml"), []byte(fmt.Sprintf("name: %s\n", m.Name))); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Generate `requirements.yaml` of the temporary local chart from the helmfile state
|
||||
reqsContent, err := yaml.Marshal(unresolved.ToChartRequirements())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := m.writeBytes(filepath.Join(wd, "requirements.yaml"), reqsContent); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Generate `requirements.lock` of the temporary local chart by coping `<basename>.lock`
|
||||
lockFile := m.lockFileName()
|
||||
|
||||
lockFileContent, err := m.readBytes(lockFile)
|
||||
if err != nil && !os.IsNotExist(err) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if lockFileContent != nil {
|
||||
if err := m.writeBytes(filepath.Join(wd, "requirements.lock"), lockFileContent); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// Update the lock file by running `helm dependency update`
|
||||
if err := shell.UpdateDeps(wd); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
updatedLockFileContent, err := m.readBytes(filepath.Join(wd, "requirements.lock"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Commit the lock file if and only if everything looks ok
|
||||
if err := m.writeBytes(lockFile, updatedLockFileContent); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resolved, _, err := m.Resolve(unresolved)
|
||||
return resolved, err
|
||||
}
|
||||
|
||||
func (m *chartDependencyManager) Resolve(unresolved *UnresolvedDependencies) (*ResolvedDependencies, bool, error) {
|
||||
updatedLockFileContent, err := m.readBytes(m.lockFileName())
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return nil, true, nil
|
||||
}
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
// Load resolved dependencies into memory
|
||||
lockedReqs := &ChartLockedRequirements{}
|
||||
if err := yaml.Unmarshal(updatedLockFileContent, lockedReqs); err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
resolved := &ResolvedDependencies{deps: map[string]ResolvedChartDependency{}}
|
||||
for _, d := range lockedReqs.ResolvedDependencies {
|
||||
if err := resolved.add(d); err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
}
|
||||
|
||||
return resolved, true, nil
|
||||
}
|
||||
|
||||
func (m *chartDependencyManager) readBytes(filename string) ([]byte, error) {
|
||||
bytes, err := m.readFile(filename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
m.logger.Debugf("readBytes: read from %s:\n%s", filename, bytes)
|
||||
return bytes, nil
|
||||
}
|
||||
|
||||
func (m *chartDependencyManager) writeBytes(filename string, data []byte) error {
|
||||
err := m.writeFile(filename, data, 0644)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
m.logger.Debugf("writeBytes: wrote to %s:\n%s", filename, data)
|
||||
return nil
|
||||
}
|
||||
|
|
@ -51,6 +51,7 @@ type HelmState struct {
|
|||
|
||||
removeFile func(string) error
|
||||
fileExists func(string) (bool, error)
|
||||
tempDir func(string, string) (string, error)
|
||||
|
||||
runner helmexec.Runner
|
||||
}
|
||||
|
|
@ -902,7 +903,7 @@ func (st *HelmState) FilterReleases() error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (st *HelmState) PrepareRelease(helm helmexec.Interface, helmfileCommand string) []error {
|
||||
func (st *HelmState) PrepareReleases(helm helmexec.Interface, helmfileCommand string) []error {
|
||||
errs := []error{}
|
||||
|
||||
for _, release := range st.Releases {
|
||||
|
|
@ -914,6 +915,14 @@ func (st *HelmState) PrepareRelease(helm helmexec.Interface, helmfileCommand str
|
|||
if len(errs) != 0 {
|
||||
return errs
|
||||
}
|
||||
|
||||
updated, err := st.ResolveDeps()
|
||||
if err != nil {
|
||||
return []error{err}
|
||||
}
|
||||
|
||||
*st = *updated
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
@ -946,6 +955,11 @@ func (st *HelmState) triggerReleaseEvent(evt string, r *ReleaseSpec, helmfileCmd
|
|||
return bus.Trigger(evt, data)
|
||||
}
|
||||
|
||||
// ResolveDeps returns a copy of this helmfile state with the concrete chart version numbers filled in for remote chart dependencies
|
||||
func (st *HelmState) ResolveDeps() (*HelmState, error) {
|
||||
return st.mergeLockedDependencies()
|
||||
}
|
||||
|
||||
// UpdateDeps wrapper for updating dependencies on the releases
|
||||
func (st *HelmState) UpdateDeps(helm helmexec.Interface) []error {
|
||||
errs := []error{}
|
||||
|
|
@ -957,6 +971,18 @@ func (st *HelmState) UpdateDeps(helm helmexec.Interface) []error {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(errs) == 0 {
|
||||
tempDir := st.tempDir
|
||||
if tempDir == nil {
|
||||
tempDir = ioutil.TempDir
|
||||
}
|
||||
_, err := st.updateDependenciesInTempDir(helm, tempDir)
|
||||
if err != nil {
|
||||
errs = append(errs, fmt.Errorf("unable to update deps: %v", err))
|
||||
}
|
||||
}
|
||||
|
||||
if len(errs) != 0 {
|
||||
return errs
|
||||
}
|
||||
|
|
@ -1008,7 +1034,12 @@ func normalizeChart(basePath, chart string) string {
|
|||
|
||||
func isLocalChart(chart string) bool {
|
||||
regex, _ := regexp.Compile("^[.]?./")
|
||||
return regex.MatchString(chart)
|
||||
matched := regex.MatchString(chart)
|
||||
if matched {
|
||||
return true
|
||||
}
|
||||
|
||||
return chart == "" || chart[0] == '/' || len(strings.Split(chart, "/")) != 2
|
||||
}
|
||||
|
||||
func pathExists(chart string) bool {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
package state
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
|
|
@ -551,7 +553,7 @@ func Test_isLocalChart(t *testing.T) {
|
|||
args: args{
|
||||
chart: "",
|
||||
},
|
||||
want: false,
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "parent local path",
|
||||
|
|
@ -567,11 +569,25 @@ func Test_isLocalChart(t *testing.T) {
|
|||
},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "absolute path",
|
||||
args: args{
|
||||
chart: "/foo/bar/baz",
|
||||
},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "local chart in 3-level deep dir",
|
||||
args: args{
|
||||
chart: "foo/bar/baz",
|
||||
},
|
||||
want: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := isLocalChart(tt.args.chart); got != tt.want {
|
||||
t.Errorf("pathExists() = %v, want %v", got, tt.want)
|
||||
t.Errorf("%s(\"%s\") isLocalChart(): got %v, want %v", tt.name, tt.args.chart, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
@ -643,6 +659,8 @@ type mockHelmExec struct {
|
|||
deleted []mockRelease
|
||||
lists map[listKey]string
|
||||
diffed []mockRelease
|
||||
|
||||
updateDepsCallbacks map[string]func(string) error
|
||||
}
|
||||
|
||||
type mockRelease struct {
|
||||
|
|
@ -658,9 +676,18 @@ type mockAffected struct {
|
|||
|
||||
func (helm *mockHelmExec) UpdateDeps(chart string) error {
|
||||
if strings.Contains(chart, "error") {
|
||||
return errors.New("error")
|
||||
return fmt.Errorf("simulated UpdateDeps failure for chart: %s", chart)
|
||||
}
|
||||
helm.charts = append(helm.charts, chart)
|
||||
|
||||
if helm.updateDepsCallbacks != nil {
|
||||
callback, exists := helm.updateDepsCallbacks[chart]
|
||||
if exists {
|
||||
if err := callback(chart); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
@ -1394,8 +1421,36 @@ func TestHelmState_DiffReleasesCleanup(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestHelmState_UpdateDeps(t *testing.T) {
|
||||
helm := &mockHelmExec{
|
||||
updateDepsCallbacks: map[string]func(string) error{},
|
||||
}
|
||||
|
||||
var generatedDir string
|
||||
tempDir := func(dir, prefix string) (string, error) {
|
||||
var err error
|
||||
generatedDir, err = ioutil.TempDir(dir, prefix)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
helm.updateDepsCallbacks[generatedDir] = func(chart string) error {
|
||||
content := []byte(`dependencies:
|
||||
- name: envoy
|
||||
repository: https://kubernetes-charts.storage.googleapis.com
|
||||
version: 1.5.0
|
||||
digest: sha256:e43b05c8528ea8ef1560f4980a519719ad2a634658abde0a98daefdb83a104e9
|
||||
generated: 2019-05-14T11:29:35.144399+09:00
|
||||
`)
|
||||
filename := filepath.Join(generatedDir, "requirements.lock")
|
||||
logger.Debugf("test: writing %s: %s", filename, content)
|
||||
return ioutil.WriteFile(filename, content, 0644)
|
||||
}
|
||||
return generatedDir, nil
|
||||
}
|
||||
|
||||
logger := helmexec.NewLogger(os.Stderr, "debug")
|
||||
state := &HelmState{
|
||||
basePath: "/src",
|
||||
FilePath: "/src/helmfile.yaml",
|
||||
Releases: []ReleaseSpec{
|
||||
{
|
||||
Chart: "./..",
|
||||
|
|
@ -1413,19 +1468,35 @@ func TestHelmState_UpdateDeps(t *testing.T) {
|
|||
Chart: "published/deeper",
|
||||
},
|
||||
{
|
||||
Chart: ".error",
|
||||
Chart: "stable/envoy",
|
||||
},
|
||||
},
|
||||
Repositories: []RepositorySpec{
|
||||
{
|
||||
Name: "stable",
|
||||
URL: "https://kubernetes-charts.storage.googleapis.com",
|
||||
},
|
||||
},
|
||||
tempDir: tempDir,
|
||||
logger: logger,
|
||||
}
|
||||
|
||||
want := []string{"/", "/examples", "/helmfile"}
|
||||
helm := &mockHelmExec{}
|
||||
errs := state.UpdateDeps(helm)
|
||||
want := []string{"/", "/examples", "/helmfile", "/src/published", generatedDir}
|
||||
if !reflect.DeepEqual(helm.charts, want) {
|
||||
t.Errorf("HelmState.UpdateDeps() = %v, want %v", helm.charts, want)
|
||||
}
|
||||
if len(errs) != 0 {
|
||||
t.Errorf("HelmState.UpdateDeps() - no errors, but got: %v", len(errs))
|
||||
t.Errorf("HelmState.UpdateDeps() - no errors, but got %d: %v", len(errs), errs)
|
||||
}
|
||||
|
||||
resolved, err := state.ResolveDeps()
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
if resolved.Releases[5].Version != "1.5.0" {
|
||||
t.Errorf("unexpected version number: expected=1.5.0, got=%s", resolved.Releases[5].Version)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -76,6 +76,16 @@ ${helmfile} -f ${dir}/happypath.yaml apply
|
|||
code=$?
|
||||
[ ${code} -eq 0 ] || fail "unexpected exit code returned by helmfile apply: ${code}"
|
||||
|
||||
info "Locking dependencies"
|
||||
${helmfile} -f ${dir}/happypath.yaml deps
|
||||
code=$?
|
||||
[ ${code} -eq 0 ] || fail "unexpected exit code returned by helmfile deps: ${code}"
|
||||
|
||||
info "Applying ${dir}/happypath.yaml with locked dependencies"
|
||||
${helmfile} -f ${dir}/happypath.yaml apply
|
||||
code=$?
|
||||
[ ${code} -eq 0 ] || fail "unexpected exit code returned by helmfile apply: ${code}"
|
||||
|
||||
info "Deleting release"
|
||||
${helmfile} -f ${dir}/happypath.yaml delete
|
||||
${helm} status --namespace=${test_ns} httpbin &> /dev/null && fail "release should not exist anymore after a delete"
|
||||
|
|
|
|||
Loading…
Reference in New Issue