Skip to content

Commit

Permalink
Merge pull request #21 from pamiel/multi-values
Browse files Browse the repository at this point in the history
Multiple values file in umbrella chart + misc
  • Loading branch information
Christophe VILA authored May 22, 2019
2 parents d72440a + 681d96a commit cc4a944
Show file tree
Hide file tree
Showing 2 changed files with 211 additions and 29 deletions.
39 changes: 38 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ dependencies:
condition: ms3.enabled
```

A "values" file shall also be set with the weight ito be applied to each individual sub-chart. This weight shall be set in the `<chart name or alias>.weight` element. A good practice is that thei weigths are statically set in the default `values.yaml` file of the umbrella chart (and not in a yaml file provided using the `-f` option), as sub-chart's weight is not likely to change over time.
A "values" file shall also be set with the weight it be applied to each individual sub-chart. This weight shall be set in the `<chart name or alias>.weight` element. A good practice is that thei weigths are statically set in the default `values.yaml` file of the umbrella chart (and not in a yaml file provided using the `-f` option), as sub-chart's weight is not likely to change over time.
As an example corresponding to the above `requirement.yaml` file, the `values.yaml` file of the umbrella chart might be:
```
micro-service-1:
Expand All @@ -73,6 +73,43 @@ ms3 7 Wed Jan 30 17:18:45 2019 DEPLOYED

Note: if an alias is set for a sub-chart, then this is this alias that should be used with the `--target` optioni, not the sub-chart name.

### Values:

The umbrella chart gathers several components or micro-services into a single solution. Values can then be set at many different places:
- At micro-service level, inside the `values.yaml` file of each micro-service chart: these are common defaults values set by the micro-service developer, independently from the deployment context and location of the micro-service
- At the solution level, inside the `values.yaml` file of the umbrella chart: these are values complementing or overwriting default values of the micro-services sub-charts, usually formalizing the deployment topology of the solution and giving the standard configuration of the underlying micro-services for any deployments of cwthis specific solution
- At deployment time, using the `--values/-f` or `--set` flags: this is the placeholder for giving the deployment-dependent values, specifying for example the exact database url for this deployment, the exact password value for this deployment, the targeted remote server url for this deployment, etc. These values usually change from one deployment of the solution to another.

Within the micro-services paradigm, decoupling between micro-services is one of the most important criteria to respect. While values con be provided in a per-micro-service basis for the first and last places mentioned above, Helm only allows one single `values.yaml` file in the umbrella chart. All solution-level values should then be gathered into a single file, while it would have been better to provide values in several files, on a one-file-per-micro-service basis (to ensure decoupling of the micro-services configuration, even at solution level).
Helm Spray is consequently adding this capability to have several values file in the umbrella chart and to include them into the single `values.yaml` file using the `#! {{ .File.Get <file name> }}` directive.
- The file to be included shall be a valid yaml file.
- It is possible to only include a sub-part of the yaml content by picking an element of the `File.Get`, specifying the path to be extracted and included: `#! {{ pick (.File.Get <file name>) for.bar }}`. Only paths targeting a Yaml element or a leaf value can be provided. Paths targeted lists are not supported.
- It is possible to indent the included content using the `indent` directive: `#! {{ .File.Get <file name> | indent 2 }}`, `#! {{ pick (.File.Get <file name>) for.bar | indent 4 }}

Note: The `{{ .File.Get ... }}` directive shall be prefixed by `#!` as the `values.yaml` file is parsed both with and without the included content. When parsed without the included content, it shall still be a valid yaml file, thus mandating the usage of a comment to specify the `{{ .File.Get ... }}` clause that is by default supported by neither yaml nor Helm in default values files of charts. Usage of `#!` (with a bang '!') allows differentiating the include clauses from regular comments.
Note also that when Helm is parsing the `values.yaml` file without the included content, some warning may be raised by helm if yaml elements are nil or empty (while they are not with the included content). A typical warning could be: 'Warning: Merging destination map for chart 'my-solution'. The destination item 'bar' is a table and ignoring the source 'bar' as it has a non-table value of: <nil>'

Example of `values.yaml`:
```
micro-service-1:
weight: 0
#! {{ .File.Get ms1.yaml }}
micro-service-2:
weight: 1
#! {{ pick (.File.Get ms2.yaml) foo | indent 2 }}
ms3:
weight: 2
bar:
#! {{ pick (.File.Get ms3.yaml) bar.baz | indent 4 }}
# To prevent from having a warning when the file is processed by Helm, a fake content may be set here.
# Format of the added dummy elements fully depends on the application's values structure
dummy:
dummy: "just here to prevent from a warning"
```

### Flags:

```
Expand Down
201 changes: 173 additions & 28 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,18 @@ import (
"fmt"
"os"
"bufio"
"io/ioutil"
"strconv"
"strings"
"regexp"
"time"
"text/tabwriter"

"github.com/gemalto/helm-spray/pkg/helm"
"github.com/gemalto/helm-spray/pkg/kubectl"

chartutil "k8s.io/helm/pkg/chartutil"
chartHapi "k8s.io/helm/pkg/proto/hapi/chart"

"github.com/spf13/cobra"
)
Expand Down Expand Up @@ -52,6 +56,7 @@ type Dependency struct {
Name string
Alias string
UsedName string
AppVersion string
Targeted bool
Weight int
CorrespondingReleaseName string
Expand Down Expand Up @@ -115,24 +120,20 @@ func newSprayCmd(args []string) *cobra.Command {

if p.chartVersion != "" {
if strings.HasSuffix(p.chartName, "tgz") {
os.Stderr.WriteString("You cannot use --version together with chart archive\n")
os.Exit(1)
logErrorAndExit("You cannot use --version together with chart archive")
}

if _, err := os.Stat(p.chartName); err == nil {
os.Stderr.WriteString("You cannot use --version together with chart directory\n")
os.Exit(1)
logErrorAndExit("You cannot use --version together with chart directory")
}

if (strings.HasPrefix(p.chartName, "http://") || strings.HasPrefix(p.chartName, "https://")) {
os.Stderr.WriteString("You cannot use --version together with chart URL\n")
os.Exit(1)
logErrorAndExit("You cannot use --version together with chart URL")
}
}

if p.prefixReleasesWithNamespace == true && p.prefixReleases != "" {
os.Stderr.WriteString("You cannot use both --prefix-releases and --prefix-releases-with-namespace together\n")
os.Exit(1)
logErrorAndExit("You cannot use both --prefix-releases and --prefix-releases-with-namespace together")
}


Expand Down Expand Up @@ -189,24 +190,57 @@ func newSprayCmd(args []string) *cobra.Command {

}

// Running Spray command
func (p *sprayCmd) spray() error {

// Load and valide the umbrella chart...
chart, err := chartutil.Load(p.chartName)
if err != nil {
panic(fmt.Errorf("%s", err))
logErrorAndExit("Error loading chart \"%s\": %s", p.chartName, err)
}

// Load and valid the requirements file...
reqs, err := chartutil.LoadRequirements(chart)
if err != nil {
panic(fmt.Errorf("%s", err))
logErrorAndExit("Error reading \"requirements.yaml\" file: %s", err)
}

// Load default values...
values, err := chartutil.CoalesceValues(chart, chart.GetValues())
if err != nil {
panic(fmt.Errorf("%s", err))
// Get the default values file of the umbrella chart and process the '#!include' directives that might be specified in it
// Only in case '--reuseValues' has not been set
var values chartutil.Values
if p.reuseValues == false {
updatedDefaultValues := processIncludeInValuesFile(chart)

// Load default values...
values, err = chartutil.CoalesceValues(chart, &chartHapi.Config{Raw: string(updatedDefaultValues)})
if err != nil {
logErrorAndExit("Error processing default values for umbrella chart: %s", err)
}

// Write default values to a temporary file and add it to the list of values files,
// for later usage during the calls to helm
tempDir, err := ioutil.TempDir("", "spray-")
if err != nil {
logErrorAndExit("Error creating temporary directory to write updated default values file for umbrella chart: %s", err)
}
defer os.RemoveAll(tempDir)

tempFile, err := ioutil.TempFile(tempDir, "updatedDefaultValues-*.yaml")
if err != nil {
logErrorAndExit("Error creating temporary file to write updated default values file for umbrella chart: %s", err)
}
defer os.Remove(tempFile.Name())

if _, err = tempFile.Write([]byte(updatedDefaultValues)); err != nil {
logErrorAndExit("Error writing updated default values file for umbrella chart into temporary file: %s", err)
}
p.valueFiles = append([]string{tempFile.Name()}, p.valueFiles...)

} else {
values, err = chartutil.CoalesceValues(chart, chart.GetValues())
if err != nil {
logErrorAndExit("Error processing default values for umbrella chart: %s", err)
}
}

// Build the list of all rependencies, and their key attributes
Expand Down Expand Up @@ -241,7 +275,7 @@ func (p *sprayCmd) spray() error {
w64 := depi["weight"].(float64)
w, err := strconv.Atoi(strconv.FormatFloat(w64, 'f', 0, 64))
if err != nil {
panic(fmt.Errorf("%s", err))
logErrorAndExit("Error computing weight value for sub-chart \"%s\": %s", dependencies[i].UsedName, err)
}
dependencies[i].Weight = w
}
Expand All @@ -254,6 +288,14 @@ func (p *sprayCmd) spray() error {
} else {
dependencies[i].CorrespondingReleaseName = dependencies[i].UsedName
}

// Get the AppVersion that is contained in the Chart.yaml file of the dependency sub-chart
for _, subChart := range chart.GetDependencies() {
if subChart.GetMetadata().GetName() == dependencies[i].Name {
dependencies[i].AppVersion = subChart.GetMetadata().GetAppVersion()
break
}
}
}


Expand Down Expand Up @@ -307,10 +349,10 @@ w.Flush()
}

if release, ok := helmReleases[dependency.CorrespondingReleaseName]; ok {
log(2, "upgrading release \"%s\": going from revision %d (status %s) to %d...", dependency.CorrespondingReleaseName, release.Revision, release.Status, release.Revision+1)
log(2, "upgrading release \"%s\": going from revision %d (status %s) to %d (appVersion %s)...", dependency.CorrespondingReleaseName, release.Revision, release.Status, release.Revision+1, dependency.AppVersion)

} else {
log(2, "upgrading release \"%s\": deploying first revision...", dependency.CorrespondingReleaseName)
log(2, "upgrading release \"%s\": deploying first revision (appVersion %s)...", dependency.CorrespondingReleaseName, dependency.AppVersion)
}

shouldWait = true
Expand Down Expand Up @@ -348,8 +390,7 @@ w.Flush()
if helmstatus.Status == "" {
log(2, "Warning: no status returned by helm.")
} else if helmstatus.Status != "DEPLOYED" {
log(2, "Error: status returned by helm differs from \"DEPLOYED\". Cannot continue spray processing.")
os.Exit(1)
logErrorAndExit("Error: status returned by helm differs from \"DEPLOYED\". Cannot continue spray processing.")
}
}
}
Expand Down Expand Up @@ -380,9 +421,7 @@ w.Flush()
}

if !done {
os.Stderr.WriteString("Error: UPGRADE FAILED: timed out waiting for the condition\n")
os.Stderr.WriteString("==> Error: exit status 1\n")
os.Exit(1)
logErrorAndExit("Error: UPGRADE FAILED: timed out waiting for the condition\n==> Error: exit status 1")
}
}

Expand All @@ -403,9 +442,7 @@ w.Flush()
}

if !done {
os.Stderr.WriteString("Error: UPGRADE FAILED: timed out waiting for the condition\n")
os.Stderr.WriteString("==> Error: exit status 1\n")
os.Exit(1)
logErrorAndExit("Error: UPGRADE FAILED: timed out waiting for the condition\n==> Error: exit status 1")
}
}

Expand All @@ -426,9 +463,7 @@ w.Flush()
}

if !done {
os.Stderr.WriteString("Error: UPGRADE FAILED: timed out waiting for the condition\n")
os.Stderr.WriteString("==> Error: exit status 1\n")
os.Exit(1)
logErrorAndExit("Error: UPGRADE FAILED: timed out waiting for the condition\n==> Error: exit status 1")
}
}
}
Expand All @@ -454,6 +489,109 @@ func getMaxWeight(v []Dependency) (m int) {
return m
}

// Search the "include" clauses in the default value file of the chart and replace them by the content
// of the corresponding file.
// Allows:
// - Includeing a file:
// #! {{ .File.Get myfile.yaml }}
// - Including a sub-part of a file, picking a specific tag. Tags can target a Yaml element (aka table) or a
// leaf value, but tags cannot target a list item.
// #! {{ pick (.File.Get myfile.yaml) tag }}
// - Indenting the include content:
// #! {{ .File.Get myfile.yaml | indent 2 }}
// - All combined...:
// #! {{ pick (.File.Get "myfile.yaml") "tag.subTag" | indent 4 }}
//
func processIncludeInValuesFile(chart *chartHapi.Chart) string {
defaultValues := string(chart.GetValues().GetRaw())

regularExpressions := []string {
// Expression #0: Process file inclusion ".File.Get" with optional "| indent"
`#!\s*\{\{\s*pick\s*\(\s*\.File\.Get\s+([a-zA-Z0-9_"\\\/.\-\(\):]+)\s*\)\s*([a-zA-Z0-9_"\.\-]+)\s*(\|\s*indent\s*(\d+))?\s*\}\}\s*\n`,
// Expression #1: Process file inclusion ".File.Get", picking a specific element of the file content "pick (.File.Get <file>) <tag>", with an optional "| indent"
`#!\s*\{\{\s*\.File\.Get\s+([a-zA-Z0-9_"\\\/.\-\(\):]+)\s*(\|\s*indent\s*(\d+))?\s*\}\}\s*\n`}

expressionNumber := 1
includeFileNameExp := regexp.MustCompile(regularExpressions[expressionNumber-1])
match := includeFileNameExp.FindStringSubmatch(defaultValues)

for ; len(match) != 0; {
var fullMatch, includeFileName, subValuePath, indent string
if expressionNumber == 1 {
fullMatch = match[0]
includeFileName = strings.Trim (match[1], `"`)
subValuePath = strings.Trim (match[2], `"`)
indent = match[4]
} else if expressionNumber == 2 {
fullMatch = match[0]
includeFileName = strings.Trim (match[1], `"`)
subValuePath = ""
indent = match[3]
}

replaced := false

for _, f := range chart.GetFiles() {
if f.GetTypeUrl() == strings.Trim(strings.TrimSpace(includeFileName), "\"") {
dataToAdd := string(f.GetValue())
if subValuePath != "" {
data, err := chartutil.ReadValues(f.GetValue())
if err != nil {
logErrorAndExit("Unable to read values from file \"%s\": %s", includeFileName, err)
}

// Suppose that the element at the path is an element (list items are not supported)
if subData, err := data.Table(subValuePath); err == nil {
if dataToAdd, err = subData.YAML(); err != nil {
logErrorAndExit("Unable to generate a valid YAML file from values at path \"%s\" in values file \"%s\": %s", subValuePath, includeFileName, err)
}

// If it is not an element, then maybe it is directly a value
} else {
if val, err2 := data.PathValue(subValuePath); err2 == nil {
var ok bool
if dataToAdd, ok = val.(string); ok == false {
logErrorAndExit("Unable to find values matching path \"%s\" in values file \"%s\": %s\n%s", subValuePath, includeFileName, err, "Targeted item is most propably a list: not supported. Only elements (aka Yaml table) and leaf values are supported.")
}

} else {
logErrorAndExit("Unable to find values matching path \"%s\" in values file \"%s\": %s", subValuePath, includeFileName, err)
}
}
}

if indent == "" {
defaultValues = strings.Replace(defaultValues, fullMatch, dataToAdd + "\n", -1)
} else {
nbrOfSpaces, err := strconv.Atoi(indent)
if err != nil {
logErrorAndExit("Error computing indentation value in \"#!include\" clause: %s", err)
}

toAdd := strings.Replace(dataToAdd, "\n", "\n" + strings.Repeat (" ", nbrOfSpaces), -1)
defaultValues = strings.Replace(defaultValues, fullMatch, strings.Repeat (" ", nbrOfSpaces) + toAdd + "\n", -1)
}

replaced = true
}
}

if !replaced {
logErrorAndExit("Unable to find file \"%s\" referenced in the \"%s\" clause of the default values file of the umbrella chart", match[1], strings.TrimRight(match[0], "\n"))
}

match = includeFileNameExp.FindStringSubmatch(defaultValues)

if len(match) == 0 && expressionNumber < len(regularExpressions) {
expressionNumber++
includeFileNameExp = regexp.MustCompile(regularExpressions[expressionNumber-1])
match = includeFileNameExp.FindStringSubmatch(defaultValues)
}
}

return defaultValues
}

// Log spray messages
func log(level int, str string, params ...interface{}) {
var logStr = "[spray] "
Expand All @@ -471,6 +609,13 @@ func log(level int, str string, params ...interface{}) {
fmt.Println(logStr + fmt.Sprintf(str, params...))
}

// Log error and exit
func logErrorAndExit(str string, params ...interface{}) {
os.Stderr.WriteString(fmt.Sprintf(str + "\n", params...))
os.Exit(1)
}


func main() {
cmd := newSprayCmd(os.Args[1:])
if err := cmd.Execute(); err != nil {
Expand Down

0 comments on commit cc4a944

Please sign in to comment.