From 1ae3814aa917f11e5d287325c056b2088913596d Mon Sep 17 00:00:00 2001 From: SteveRuble Date: Thu, 13 Dec 2018 11:11:11 -0500 Subject: [PATCH] feat: Support local charts on file system This adds support for charts that are on the file system rather than in a repo. It works by checking whether a chart has a repo that's listed in the repos section of the DSF. If it doesn't, the chart name is treated as a file path instead. If the chart is not present on the file system, or the chart on the file system is a different version than specified in the DSF, validation fails. --- decision_maker.go | 13 +++++++--- docs/how_to/use_local_charts.md | 15 +++++++++-- helm_helpers.go | 45 ++++++++++++++++++++++++++------- release.go | 2 +- utils.go | 19 +++++++++++++- 5 files changed, 78 insertions(+), 16 deletions(-) diff --git a/decision_maker.go b/decision_maker.go index 917ba5a3..4e0255be 100644 --- a/decision_maker.go +++ b/decision_maker.go @@ -2,6 +2,7 @@ package main import ( "fmt" + "regexp" "strconv" "strings" ) @@ -268,7 +269,7 @@ func reInstallRelease(r *release, rs releaseState) { installCmd := command{ Cmd: "bash", - Args: []string{"-c", "helm install " + r.Chart + " --version " + r.Version + " -n " + r.Name + " --namespace " + r.Namespace + getValuesFiles(r) + getSetValues(r) + getSetStringValues(r) + getWait(r) + getDesiredTillerNamespaceFlag(r) + getTLSFlags(r) + getTimeout(r) + getNoHooks(r) + getDryRunFlags()}, + Args: []string{"-c", "helm install " + r.Chart+ " --version " + r.Version + " -n " + r.Name + " --namespace " + r.Namespace + getValuesFiles(r) + getSetValues(r) + getSetStringValues(r) + getWait(r) + getDesiredTillerNamespaceFlag(r) + getTLSFlags(r) + getTimeout(r) + getNoHooks(r) + getDryRunFlags()}, Description: "installing release [ " + r.Name + " ] in namespace [[ " + r.Namespace + " ]] using Tiller in [ " + getDesiredTillerNamespace(r) + " ]", } outcome.addCommand(installCmd, r.Priority, r) @@ -283,13 +284,19 @@ func logDecision(decision string, priority int) { } // extractChartName extracts the Helm chart name from full chart name in the desired state. -// example: it extracts "chartY" from "repoX/chartY" +// example: it extracts "chartY" from "repoX/chartY" and "chartZ" from "c:\charts\chartZ" func extractChartName(releaseChart string) string { - return strings.TrimSpace(strings.Split(releaseChart, "/")[1]) + m := chartNameExtractor.FindStringSubmatch(releaseChart) + if len(m) == 2 { + return m[1] + } + return "" } +var chartNameExtractor = regexp.MustCompile(`[\\/]([^\\/]+)$`) + // getNoHooks returns the no-hooks flag for install/upgrade commands func getNoHooks(r *release) string { if r.NoHooks { diff --git a/docs/how_to/use_local_charts.md b/docs/how_to/use_local_charts.md index cc9c0574..487ba4c6 100644 --- a/docs/how_to/use_local_charts.md +++ b/docs/how_to/use_local_charts.md @@ -4,7 +4,11 @@ version: v1.3.0-rc # use local helm charts -You can use your locally developed charts. But first, you have to serve them on localhost using helm's `serve` option. +You can use your locally developed charts. + +## Served by Helm + +You can serve them on localhost using helm's `serve` option. ```toml ... @@ -28,4 +32,11 @@ helmRepos: ... -``` \ No newline at end of file +``` + +## From file system + +If you use a file path (relative to the DSF, or absolute) for the ```chart``` attribute +helmsman will try to resolve that chart from the local file system. The chart on the +local file system must have a version matching the version specified in the DSF. + diff --git a/helm_helpers.go b/helm_helpers.go index 29fe138e..71d96c46 100644 --- a/helm_helpers.go +++ b/helm_helpers.go @@ -3,13 +3,14 @@ package main import ( "encoding/json" "log" + "path/filepath" "regexp" "strconv" "strings" "time" "github.com/Praqma/helmsman/gcs" - version "github.com/hashicorp/go-version" + "github.com/hashicorp/go-version" ) var currentState map[string]releaseState @@ -211,18 +212,44 @@ func getNSTLSFlags(ns string) string { // Valid charts are the ones that can be found in the defined repos. // This function uses Helm search to verify if the chart can be found or not. func validateReleaseCharts(apps map[string]*release) (bool, string) { + versionExtractor := regexp.MustCompile(`version:\s?(.*)`) for app, r := range apps { - cmd := command{ - Cmd: "bash", - Args: []string{"-c", "helm search " + r.Chart + " --version " + strconv.Quote(r.Version) + " -l"}, - Description: "validating if chart " + r.Chart + "-" + r.Version + " is available in the defined repos.", - } - if exitCode, result := cmd.exec(debug, verbose); exitCode != 0 || strings.Contains(result, "No results found") { - return false, "ERROR: chart " + r.Chart + "-" + r.Version + " is specified for " + - "app [" + app + "] but is not found in the defined repos." + isLocal := filepath.IsAbs(r.Chart) + if isLocal { + cmd := command{ + Cmd: "bash", + Args: []string{"-c", "helm inspect chart " + r.Chart}, + Description: "validating if chart at " + r.Chart + " is available.", + } + + if exitCode, output := cmd.exec(debug, verbose); exitCode != 0 { + maybeRepo := filepath.Base(filepath.Dir(r.Chart)) + return false, "ERROR: chart at " + r.Chart + " for app [" + app + "] could not be found. Did you mean to add a repo named '" + maybeRepo +"'?" + } else { + matches := versionExtractor.FindStringSubmatch(output) + if len(matches) == 2 { + version := matches[1] + if r.Version != version { + return false, "ERROR: chart " + r.Chart + " with version " + r.Version + " is specified for " + + "app [" + app + "] but the chart found at that path has version " + version + " which does not match." + } + } + } + } else { + cmd := command{ + Cmd: "bash", + Args: []string{"-c", "helm search " + r.Chart + " --version " + strconv.Quote(r.Version) + " -l"}, + Description: "validating if chart " + r.Chart + "-" + r.Version + " is available in the defined repos.", + } + + if exitCode, result := cmd.exec(debug, verbose); exitCode != 0 || strings.Contains(result, "No results found") { + return false, "ERROR: chart " + r.Chart + "-" + r.Version + " is specified for " + + "app [" + app + "] but is not found in the defined repos." + } } + } return true, "" } diff --git a/release.go b/release.go index 457e85db..f7eb7616 100644 --- a/release.go +++ b/release.go @@ -6,7 +6,7 @@ import ( "os" "strings" - version "github.com/hashicorp/go-version" + "github.com/hashicorp/go-version" ) // release type representing Helm releases which are described in the desired state diff --git a/utils.go b/utils.go index 0199858c..210b4858 100644 --- a/utils.go +++ b/utils.go @@ -143,6 +143,7 @@ func toFile(file string, s *state) { func resolvePaths(relativeToFile string, s *state) { dir := filepath.Dir(relativeToFile) + for k, v := range s.Apps { if v.ValuesFile != "" { v.ValuesFile, _ = filepath.Abs(filepath.Join(dir, v.ValuesFile)) @@ -156,9 +157,25 @@ func resolvePaths(relativeToFile string, s *state) { for i, f := range v.SecretsFiles { v.SecretsFiles[i], _ = filepath.Abs(filepath.Join(dir, f)) } + + if v.Chart != "" { + var repoOrDir = filepath.Dir(v.Chart) + _, isRepo := s.HelmRepos[repoOrDir] + if !isRepo { + // if there is no repo for the chart, we assume it's intended to be a local path + + // support env vars in path + v.Chart = os.ExpandEnv(v.Chart) + // respect absolute paths to charts but resolve relative paths + if !filepath.IsAbs(v.Chart) { + v.Chart, _ = filepath.Abs(filepath.Join(dir, v.Chart)) + } + } + } + s.Apps[k] = v } - //resolving paths for k8s certificate files + // resolving paths for k8s certificate files for k, v := range s.Certificates { if _, err := url.ParseRequestURI(v); err != nil { v, _ = filepath.Abs(filepath.Join(dir, v))