Merge branch 'dev-v3' into output-dir

pull/5762/head
Torsten Walter 6 years ago
commit 7347be61ee

2
Gopkg.lock generated

@ -1710,7 +1710,7 @@
"k8s.io/client-go/tools/clientcmd", "k8s.io/client-go/tools/clientcmd",
"k8s.io/client-go/tools/watch", "k8s.io/client-go/tools/watch",
"k8s.io/client-go/util/homedir", "k8s.io/client-go/util/homedir",
"k8s.io/kubernetes/pkg/api/legacyscheme", "k8s.io/klog",
"k8s.io/kubernetes/pkg/controller/deployment/util", "k8s.io/kubernetes/pkg/controller/deployment/util",
"k8s.io/kubernetes/pkg/kubectl/cmd/testing", "k8s.io/kubernetes/pkg/kubectl/cmd/testing",
"k8s.io/kubernetes/pkg/kubectl/cmd/util", "k8s.io/kubernetes/pkg/kubectl/cmd/util",

@ -17,13 +17,18 @@ limitations under the License.
package main // import "helm.sh/helm/cmd/helm" package main // import "helm.sh/helm/cmd/helm"
import ( import (
"flag"
"fmt" "fmt"
"log" "log"
"os" "os"
"strings"
"sync" "sync"
// Import to initialize client auth plugins. "github.com/spf13/pflag"
"k8s.io/cli-runtime/pkg/genericclioptions" "k8s.io/cli-runtime/pkg/genericclioptions"
"k8s.io/klog"
// Import to initialize client auth plugins.
_ "k8s.io/client-go/plugin/pkg/client/auth" _ "k8s.io/client-go/plugin/pkg/client/auth"
"helm.sh/helm/pkg/action" "helm.sh/helm/pkg/action"
@ -50,7 +55,16 @@ func logf(format string, v ...interface{}) {
} }
} }
func initKubeLogs() {
pflag.CommandLine.SetNormalizeFunc(wordSepNormalizeFunc)
gofs := flag.NewFlagSet("klog", flag.ExitOnError)
klog.InitFlags(gofs)
pflag.CommandLine.AddGoFlagSet(gofs)
pflag.CommandLine.Set("logtostderr", "true")
}
func main() { func main() {
initKubeLogs()
cmd := newRootCmd(newActionConfig(false), os.Stdout, os.Args[1:]) cmd := newRootCmd(newActionConfig(false), os.Stdout, os.Args[1:])
if err := cmd.Execute(); err != nil { if err := cmd.Execute(); err != nil {
logf("%+v", err) logf("%+v", err)
@ -111,3 +125,8 @@ func getNamespace() string {
} }
return "default" return "default"
} }
// wordSepNormalizeFunc changes all flags that contain "_" separators
func wordSepNormalizeFunc(f *pflag.FlagSet, name string) pflag.NormalizedName {
return pflag.NormalizedName(strings.ReplaceAll(name, "_", "-"))
}

@ -20,17 +20,17 @@ import (
"io" "io"
"time" "time"
"helm.sh/helm/pkg/release" "github.com/pkg/errors"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/spf13/pflag" "github.com/spf13/pflag"
"helm.sh/helm/cmd/helm/require" "helm.sh/helm/cmd/helm/require"
"helm.sh/helm/pkg/action" "helm.sh/helm/pkg/action"
"helm.sh/helm/pkg/chart"
"helm.sh/helm/pkg/chart/loader" "helm.sh/helm/pkg/chart/loader"
"helm.sh/helm/pkg/chartutil"
"helm.sh/helm/pkg/downloader" "helm.sh/helm/pkg/downloader"
"helm.sh/helm/pkg/getter" "helm.sh/helm/pkg/getter"
"helm.sh/helm/pkg/release"
) )
const installDesc = ` const installDesc = `
@ -180,7 +180,7 @@ func runInstall(args []string, client *action.Install, out io.Writer) (*release.
return nil, err return nil, err
} }
validInstallableChart, err := chartutil.IsChartInstallable(chartRequested) validInstallableChart, err := isChartInstallable(chartRequested)
if !validInstallableChart { if !validInstallableChart {
return nil, err return nil, err
} }
@ -211,3 +211,14 @@ func runInstall(args []string, client *action.Install, out io.Writer) (*release.
client.Namespace = getNamespace() client.Namespace = getNamespace()
return client.Run(chartRequested) return client.Run(chartRequested)
} }
// isChartInstallable validates if a chart can be installed
//
// Application chart type is only installable
func isChartInstallable(ch *chart.Chart) (bool, error) {
switch ch.Metadata.Type {
case "", "application":
return true, nil
}
return false, errors.Errorf("%s charts are not installable", ch.Metadata.Type)
}

@ -22,11 +22,10 @@ import (
"io/ioutil" "io/ioutil"
"path/filepath" "path/filepath"
"helm.sh/helm/pkg/action"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"helm.sh/helm/pkg/action"
"helm.sh/helm/pkg/downloader" "helm.sh/helm/pkg/downloader"
"helm.sh/helm/pkg/getter" "helm.sh/helm/pkg/getter"
) )

@ -56,7 +56,7 @@ func newRollbackCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
} }
f := cmd.Flags() f := cmd.Flags()
f.IntVarP(&client.Version, "version", "v", 0, "revision number to rollback to (default: rollback to previous release)") f.IntVar(&client.Version, "version", 0, "revision number to rollback to (default: rollback to previous release)")
f.BoolVar(&client.DryRun, "dry-run", false, "simulate a rollback") f.BoolVar(&client.DryRun, "dry-run", false, "simulate a rollback")
f.BoolVar(&client.Recreate, "recreate-pods", false, "performs pods restart for the resource if applicable") f.BoolVar(&client.Recreate, "recreate-pods", false, "performs pods restart for the resource if applicable")
f.BoolVar(&client.Force, "force", false, "force resource update through delete/recreate if needed") f.BoolVar(&client.Force, "force", false, "force resource update through delete/recreate if needed")

@ -65,7 +65,7 @@ func newSearchCmd(out io.Writer) *cobra.Command {
f := cmd.Flags() f := cmd.Flags()
f.BoolVarP(&o.regexp, "regexp", "r", false, "use regular expressions for searching") f.BoolVarP(&o.regexp, "regexp", "r", false, "use regular expressions for searching")
f.BoolVarP(&o.versions, "versions", "l", false, "show the long listing, with each version of each chart on its own line") f.BoolVarP(&o.versions, "versions", "l", false, "show the long listing, with each version of each chart on its own line")
f.StringVarP(&o.version, "version", "v", "", "search using semantic versioning constraints") f.StringVar(&o.version, "version", "", "search using semantic versioning constraints")
return cmd return cmd
} }

@ -21,7 +21,6 @@ import (
"time" "time"
"helm.sh/helm/pkg/chart" "helm.sh/helm/pkg/chart"
"helm.sh/helm/pkg/release" "helm.sh/helm/pkg/release"
) )

@ -1 +1 @@
Error: Invalid chart types are not installable Error: validation: chart.metadata.type must be application or library

@ -1 +1 @@
Error: Library charts are not installable Error: validation: chart.metadata.type must be application or library

@ -175,6 +175,12 @@ Additionally, several other commands were re-named to accommodate the same conve
These commands have also retained their older verbs as aliases, so you can continue to use them in either form. These commands have also retained their older verbs as aliases, so you can continue to use them in either form.
### Automatically creating namespaces
When creating a release in a namespace that does not exist, Helm 2 created the
namespace. Helm 3 follows the behavior of other Kubernetes objects and returns
an error if the namespace does not exist.
## Installing ## Installing
### Why aren't there Debian/Fedora/... native packages of Helm? ### Why aren't there Debian/Fedora/... native packages of Helm?

@ -59,11 +59,6 @@ func (p *Package) Run(path string) (string, error) {
return "", err return "", err
} }
validChartType, err := chartutil.IsValidChartType(ch)
if !validChartType {
return "", err
}
combinedVals, err := chartutil.CoalesceValues(ch, p.ValueOptions.rawValues) combinedVals, err := chartutil.CoalesceValues(ch, p.ValueOptions.rawValues)
if err != nil { if err != nil {
return "", err return "", err
@ -108,7 +103,7 @@ func (p *Package) Run(path string) (string, error) {
err = p.Clearsign(name) err = p.Clearsign(name)
} }
return "", err return name, err
} }
func setVersion(ch *chart.Chart, ver string) error { func setVersion(ch *chart.Chart, ver string) error {

@ -1,11 +1,10 @@
/* /*
Copyright The Helm Authors. Copyright The Helm Authors.
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
You may obtain a copy of the License at You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0 http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS, distributed under the License is distributed on an "AS IS" BASIS,
@ -14,17 +13,11 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
package kube package chart
import ( // ValidationError represents a data validation error.
"flag" type ValidationError string
"fmt"
"os"
)
func init() { func (v ValidationError) Error() string {
if level := os.Getenv("KUBE_LOG_LEVEL"); level != "" { return "validation: " + string(v)
flag.Set("vmodule", fmt.Sprintf("loader=%[1]s,round_trippers=%[1]s,request=%[1]s", level))
flag.Set("logtostderr", "true")
}
} }

@ -127,11 +127,10 @@ icon: https://example.com/64x64.png
t.Errorf("Expected number of templates == 2, got %d", len(c.Templates)) t.Errorf("Expected number of templates == 2, got %d", len(c.Templates))
} }
_, err = LoadFiles([]*BufferedFile{}) if _, err = LoadFiles([]*BufferedFile{}); err == nil {
if err == nil {
t.Fatal("Expected err to be non-nil") t.Fatal("Expected err to be non-nil")
} }
if err.Error() != "metadata is required" { if err.Error() != "validation: chart.metadata is required" {
t.Errorf("Expected chart metadata missing error, got '%s'", err.Error()) t.Errorf("Expected chart metadata missing error, got '%s'", err.Error())
} }
} }

@ -15,8 +15,6 @@ limitations under the License.
package chart package chart
import "errors"
// Maintainer describes a Chart maintainer. // Maintainer describes a Chart maintainer.
type Maintainer struct { type Maintainer struct {
// Name is a user name or organization name // Name is a user name or organization name
@ -70,17 +68,28 @@ type Metadata struct {
func (md *Metadata) Validate() error { func (md *Metadata) Validate() error {
if md == nil { if md == nil {
return errors.New("metadata is required") return ValidationError("chart.metadata is required")
} }
if md.APIVersion == "" { if md.APIVersion == "" {
return errors.New("metadata apiVersion is required") return ValidationError("chart.metadata.apiVersion is required")
} }
if md.Name == "" { if md.Name == "" {
return errors.New("metadata name is required") return ValidationError("chart.metadata.name is required")
} }
if md.Version == "" { if md.Version == "" {
return errors.New("metadata version is required") return ValidationError("chart.metadata.version is required")
}
if !isValidChartType(md.Type) {
return ValidationError("chart.metadata.type must be application or library")
} }
// TODO validate valid semver here? // TODO validate valid semver here?
return nil return nil
} }
func isValidChartType(in string) bool {
switch in {
case "", "application", "library":
return true
}
return false
}

@ -20,7 +20,6 @@ import (
"io/ioutil" "io/ioutil"
"os" "os"
"path/filepath" "path/filepath"
"strings"
"github.com/ghodss/yaml" "github.com/ghodss/yaml"
"github.com/pkg/errors" "github.com/pkg/errors"
@ -83,42 +82,3 @@ func IsChartDir(dirName string) (bool, error) {
return true, nil return true, nil
} }
// IsChartInstallable validates if a chart can be installed
//
// Application chart type is only installable
func IsChartInstallable(chart *chart.Chart) (bool, error) {
if IsLibraryChart(chart) {
return false, errors.New("Library charts are not installable")
}
validChartType, _ := IsValidChartType(chart)
if !validChartType {
return false, errors.New("Invalid chart types are not installable")
}
return true, nil
}
// IsValidChartType validates the chart type
//
// Valid types are: application or library
func IsValidChartType(chart *chart.Chart) (bool, error) {
chartType := chart.Metadata.Type
if chartType != "" && !strings.EqualFold(chartType, "library") &&
!strings.EqualFold(chartType, "application") {
return false, errors.New("Invalid chart type. Valid types are: application or library")
}
return true, nil
}
// IsLibraryChart returns true if the chart is a library chart
func IsLibraryChart(c *chart.Chart) bool {
return strings.EqualFold(c.Metadata.Type, "library")
}
// IsTemplateValid returns true if the template is valid for the chart type
func IsTemplateValid(templateName string, isLibChart bool) bool {
if isLibChart {
return strings.HasPrefix(filepath.Base(templateName), "_")
}
return true
}

@ -0,0 +1,195 @@
/*
Copyright The Helm Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package chartutil
import (
"log"
"github.com/pkg/errors"
"helm.sh/helm/pkg/chart"
)
// CoalesceValues coalesces all of the values in a chart (and its subcharts).
//
// Values are coalesced together using the following rules:
//
// - Values in a higher level chart always override values in a lower-level
// dependency chart
// - Scalar values and arrays are replaced, maps are merged
// - A chart has access to all of the variables for it, as well as all of
// the values destined for its dependencies.
func CoalesceValues(chrt *chart.Chart, vals map[string]interface{}) (Values, error) {
if vals == nil {
vals = make(map[string]interface{})
}
if _, err := coalesce(chrt, vals); err != nil {
return vals, err
}
return coalesceDeps(chrt, vals)
}
// coalesce coalesces the dest values and the chart values, giving priority to the dest values.
//
// This is a helper function for CoalesceValues.
func coalesce(ch *chart.Chart, dest map[string]interface{}) (map[string]interface{}, error) {
coalesceValues(ch, dest)
return coalesceDeps(ch, dest)
}
// coalesceDeps coalesces the dependencies of the given chart.
func coalesceDeps(chrt *chart.Chart, dest map[string]interface{}) (map[string]interface{}, error) {
for _, subchart := range chrt.Dependencies() {
if c, ok := dest[subchart.Name()]; !ok {
// If dest doesn't already have the key, create it.
dest[subchart.Name()] = make(map[string]interface{})
} else if !istable(c) {
return dest, errors.Errorf("type mismatch on %s: %t", subchart.Name(), c)
}
if dv, ok := dest[subchart.Name()]; ok {
dvmap := dv.(map[string]interface{})
// Get globals out of dest and merge them into dvmap.
coalesceGlobals(dvmap, dest)
// Now coalesce the rest of the values.
var err error
dest[subchart.Name()], err = coalesce(subchart, dvmap)
if err != nil {
return dest, err
}
}
}
return dest, nil
}
// coalesceGlobals copies the globals out of src and merges them into dest.
//
// For convenience, returns dest.
func coalesceGlobals(dest, src map[string]interface{}) {
var dg, sg map[string]interface{}
if destglob, ok := dest[GlobalKey]; !ok {
dg = make(map[string]interface{})
} else if dg, ok = destglob.(map[string]interface{}); !ok {
log.Printf("warning: skipping globals because destination %s is not a table.", GlobalKey)
return
}
if srcglob, ok := src[GlobalKey]; !ok {
sg = make(map[string]interface{})
} else if sg, ok = srcglob.(map[string]interface{}); !ok {
log.Printf("warning: skipping globals because source %s is not a table.", GlobalKey)
return
}
// EXPERIMENTAL: In the past, we have disallowed globals to test tables. This
// reverses that decision. It may somehow be possible to introduce a loop
// here, but I haven't found a way. So for the time being, let's allow
// tables in globals.
for key, val := range sg {
if istable(val) {
vv := copyMap(val.(map[string]interface{}))
if destv, ok := dg[key]; !ok {
// Here there is no merge. We're just adding.
dg[key] = vv
} else {
if destvmap, ok := destv.(map[string]interface{}); !ok {
log.Printf("Conflict: cannot merge map onto non-map for %q. Skipping.", key)
} else {
// Basically, we reverse order of coalesce here to merge
// top-down.
CoalesceTables(vv, destvmap)
dg[key] = vv
continue
}
}
} else if dv, ok := dg[key]; ok && istable(dv) {
// It's not clear if this condition can actually ever trigger.
log.Printf("key %s is table. Skipping", key)
continue
}
// TODO: Do we need to do any additional checking on the value?
dg[key] = val
}
dest[GlobalKey] = dg
}
func copyMap(src map[string]interface{}) map[string]interface{} {
m := make(map[string]interface{}, len(src))
for k, v := range src {
m[k] = v
}
return m
}
// coalesceValues builds up a values map for a particular chart.
//
// Values in v will override the values in the chart.
func coalesceValues(c *chart.Chart, v map[string]interface{}) {
for key, val := range c.Values {
if value, ok := v[key]; ok {
if value == nil {
// When the YAML value is null, we remove the value's key.
// This allows Helm's various sources of values (value files or --set) to
// remove incompatible keys from any previous chart, file, or set values.
delete(v, key)
} else if dest, ok := value.(map[string]interface{}); ok {
// if v[key] is a table, merge nv's val table into v[key].
src, ok := val.(map[string]interface{})
if !ok {
log.Printf("warning: skipped value for %s: Not a table.", key)
continue
}
// Because v has higher precedence than nv, dest values override src
// values.
CoalesceTables(dest, src)
}
} else {
// If the key is not in v, copy it from nv.
v[key] = val
}
}
}
// CoalesceTables merges a source map into a destination map.
//
// dest is considered authoritative.
func CoalesceTables(dst, src map[string]interface{}) map[string]interface{} {
if dst == nil || src == nil {
return src
}
// Because dest has higher precedence than src, dest values override src
// values.
for key, val := range src {
if istable(val) {
switch innerdst, ok := dst[key]; {
case !ok:
dst[key] = val
case istable(innerdst):
CoalesceTables(innerdst.(map[string]interface{}), val.(map[string]interface{}))
default:
log.Printf("warning: cannot overwrite table with non table for %s (%v)", key, val)
}
} else if dv, ok := dst[key]; ok && istable(dv) {
log.Printf("warning: destination for %s is a table. Ignoring non-table value %v", key, val)
} else if !ok { // <- ok is still in scope from preceding conditional.
dst[key] = val
}
}
return dst
}

@ -0,0 +1,168 @@
/*
Copyright The Helm Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package chartutil
import (
"encoding/json"
"testing"
)
// ref: http://www.yaml.org/spec/1.2/spec.html#id2803362
var testCoalesceValuesYaml = []byte(`
top: yup
bottom: null
right: Null
left: NULL
front: ~
back: ""
global:
name: Ishmael
subject: Queequeg
nested:
boat: true
pequod:
global:
name: Stinky
harpooner: Tashtego
nested:
boat: false
sail: true
ahab:
scope: whale
`)
func TestCoalesceValues(t *testing.T) {
c := loadChart(t, "testdata/moby")
vals, err := ReadValues(testCoalesceValuesYaml)
if err != nil {
t.Fatal(err)
}
v, err := CoalesceValues(c, vals)
if err != nil {
t.Fatal(err)
}
j, _ := json.MarshalIndent(v, "", " ")
t.Logf("Coalesced Values: %s", string(j))
tests := []struct {
tpl string
expect string
}{
{"{{.top}}", "yup"},
{"{{.back}}", ""},
{"{{.name}}", "moby"},
{"{{.global.name}}", "Ishmael"},
{"{{.global.subject}}", "Queequeg"},
{"{{.global.harpooner}}", "<no value>"},
{"{{.pequod.name}}", "pequod"},
{"{{.pequod.ahab.name}}", "ahab"},
{"{{.pequod.ahab.scope}}", "whale"},
{"{{.pequod.ahab.global.name}}", "Ishmael"},
{"{{.pequod.ahab.global.subject}}", "Queequeg"},
{"{{.pequod.ahab.global.harpooner}}", "Tashtego"},
{"{{.pequod.global.name}}", "Ishmael"},
{"{{.pequod.global.subject}}", "Queequeg"},
{"{{.spouter.global.name}}", "Ishmael"},
{"{{.spouter.global.harpooner}}", "<no value>"},
{"{{.global.nested.boat}}", "true"},
{"{{.pequod.global.nested.boat}}", "true"},
{"{{.spouter.global.nested.boat}}", "true"},
{"{{.pequod.global.nested.sail}}", "true"},
{"{{.spouter.global.nested.sail}}", "<no value>"},
}
for _, tt := range tests {
if o, err := ttpl(tt.tpl, v); err != nil || o != tt.expect {
t.Errorf("Expected %q to expand to %q, got %q", tt.tpl, tt.expect, o)
}
}
nullKeys := []string{"bottom", "right", "left", "front"}
for _, nullKey := range nullKeys {
if _, ok := v[nullKey]; ok {
t.Errorf("Expected key %q to be removed, still present", nullKey)
}
}
}
func TestCoalesceTables(t *testing.T) {
dst := map[string]interface{}{
"name": "Ishmael",
"address": map[string]interface{}{
"street": "123 Spouter Inn Ct.",
"city": "Nantucket",
},
"details": map[string]interface{}{
"friends": []string{"Tashtego"},
},
"boat": "pequod",
}
src := map[string]interface{}{
"occupation": "whaler",
"address": map[string]interface{}{
"state": "MA",
"street": "234 Spouter Inn Ct.",
},
"details": "empty",
"boat": map[string]interface{}{
"mast": true,
},
}
// What we expect is that anything in dst overrides anything in src, but that
// otherwise the values are coalesced.
CoalesceTables(dst, src)
if dst["name"] != "Ishmael" {
t.Errorf("Unexpected name: %s", dst["name"])
}
if dst["occupation"] != "whaler" {
t.Errorf("Unexpected occupation: %s", dst["occupation"])
}
addr, ok := dst["address"].(map[string]interface{})
if !ok {
t.Fatal("Address went away.")
}
if addr["street"].(string) != "123 Spouter Inn Ct." {
t.Errorf("Unexpected address: %v", addr["street"])
}
if addr["city"].(string) != "Nantucket" {
t.Errorf("Unexpected city: %v", addr["city"])
}
if addr["state"].(string) != "MA" {
t.Errorf("Unexpected state: %v", addr["state"])
}
if det, ok := dst["details"].(map[string]interface{}); !ok {
t.Fatalf("Details is the wrong type: %v", dst["details"])
} else if _, ok := det["friends"]; !ok {
t.Error("Could not find your friends. Maybe you don't have any. :-(")
}
if dst["boat"].(string) != "pequod" {
t.Errorf("Expected boat string, got %v", dst["boat"])
}
}

@ -0,0 +1,31 @@
/*
Copyright The Helm Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package chartutil
import (
"fmt"
)
// ErrNoTable indicates that a chart does not have a matching table.
type ErrNoTable string
func (e ErrNoTable) Error() string { return fmt.Sprintf("%q is not a table", e) }
// ErrNoValue indicates that Values does not contain a key with a value
type ErrNoValue string
func (e ErrNoValue) Error() string { return fmt.Sprintf("%q is not a value", e) }

@ -0,0 +1,87 @@
/*
Copyright The Helm Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package chartutil
import (
"bytes"
"fmt"
"strings"
"github.com/ghodss/yaml"
"github.com/pkg/errors"
"github.com/xeipuuv/gojsonschema"
"helm.sh/helm/pkg/chart"
)
// ValidateAgainstSchema checks that values does not violate the structure laid out in schema
func ValidateAgainstSchema(chrt *chart.Chart, values map[string]interface{}) error {
var sb strings.Builder
if chrt.Schema != nil {
err := ValidateAgainstSingleSchema(values, chrt.Schema)
if err != nil {
sb.WriteString(fmt.Sprintf("%s:\n", chrt.Name()))
sb.WriteString(err.Error())
}
}
// For each dependency, recurively call this function with the coalesced values
for _, subchrt := range chrt.Dependencies() {
subchrtValues := values[subchrt.Name()].(map[string]interface{})
if err := ValidateAgainstSchema(subchrt, subchrtValues); err != nil {
sb.WriteString(err.Error())
}
}
if sb.Len() > 0 {
return errors.New(sb.String())
}
return nil
}
// ValidateAgainstSingleSchema checks that values does not violate the structure laid out in this schema
func ValidateAgainstSingleSchema(values Values, schemaJSON []byte) error {
valuesData, err := yaml.Marshal(values)
if err != nil {
return err
}
valuesJSON, err := yaml.YAMLToJSON(valuesData)
if err != nil {
return err
}
if bytes.Equal(valuesJSON, []byte("null")) {
valuesJSON = []byte("{}")
}
schemaLoader := gojsonschema.NewBytesLoader(schemaJSON)
valuesLoader := gojsonschema.NewBytesLoader(valuesJSON)
result, err := gojsonschema.Validate(schemaLoader, valuesLoader)
if err != nil {
return err
}
if !result.Valid() {
var sb strings.Builder
for _, desc := range result.Errors() {
sb.WriteString(fmt.Sprintf("- %s\n", desc))
}
return errors.New(sb.String())
}
return nil
}

@ -0,0 +1,143 @@
/*
Copyright The Helm Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package chartutil
import (
"io/ioutil"
"testing"
"helm.sh/helm/pkg/chart"
)
func TestValidateAgainstSingleSchema(t *testing.T) {
values, err := ReadValuesFile("./testdata/test-values.yaml")
if err != nil {
t.Fatalf("Error reading YAML file: %s", err)
}
schema, err := ioutil.ReadFile("./testdata/test-values.schema.json")
if err != nil {
t.Fatalf("Error reading YAML file: %s", err)
}
if err := ValidateAgainstSingleSchema(values, schema); err != nil {
t.Errorf("Error validating Values against Schema: %s", err)
}
}
func TestValidateAgainstSingleSchemaNegative(t *testing.T) {
values, err := ReadValuesFile("./testdata/test-values-negative.yaml")
if err != nil {
t.Fatalf("Error reading YAML file: %s", err)
}
schema, err := ioutil.ReadFile("./testdata/test-values.schema.json")
if err != nil {
t.Fatalf("Error reading YAML file: %s", err)
}
var errString string
if err := ValidateAgainstSingleSchema(values, schema); err == nil {
t.Fatalf("Expected an error, but got nil")
} else {
errString = err.Error()
}
expectedErrString := `- (root): employmentInfo is required
- age: Must be greater than or equal to 0/1
`
if errString != expectedErrString {
t.Errorf("Error string :\n`%s`\ndoes not match expected\n`%s`", errString, expectedErrString)
}
}
const subchrtSchema = `{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Values",
"type": "object",
"properties": {
"age": {
"description": "Age",
"minimum": 0,
"type": "integer"
}
},
"required": [
"age"
]
}
`
func TestValidateAgainstSchema(t *testing.T) {
subchrtJSON := []byte(subchrtSchema)
subchrt := &chart.Chart{
Metadata: &chart.Metadata{
Name: "subchrt",
},
Schema: subchrtJSON,
}
chrt := &chart.Chart{
Metadata: &chart.Metadata{
Name: "chrt",
},
}
chrt.AddDependency(subchrt)
vals := map[string]interface{}{
"name": "John",
"subchrt": map[string]interface{}{
"age": 25,
},
}
if err := ValidateAgainstSchema(chrt, vals); err != nil {
t.Errorf("Error validating Values against Schema: %s", err)
}
}
func TestValidateAgainstSchemaNegative(t *testing.T) {
subchrtJSON := []byte(subchrtSchema)
subchrt := &chart.Chart{
Metadata: &chart.Metadata{
Name: "subchrt",
},
Schema: subchrtJSON,
}
chrt := &chart.Chart{
Metadata: &chart.Metadata{
Name: "chrt",
},
}
chrt.AddDependency(subchrt)
vals := map[string]interface{}{
"name": "John",
"subchrt": map[string]interface{}{},
}
var errString string
if err := ValidateAgainstSchema(chrt, vals); err == nil {
t.Fatalf("Expected an error, but got nil")
} else {
errString = err.Error()
}
expectedErrString := `subchrt:
- (root): age is required
`
if errString != expectedErrString {
t.Errorf("Error string :\n`%s`\ndoes not match expected\n`%s`", errString, expectedErrString)
}
}

@ -17,30 +17,17 @@ limitations under the License.
package chartutil package chartutil
import ( import (
"bytes"
"fmt" "fmt"
"io" "io"
"io/ioutil" "io/ioutil"
"log"
"strings" "strings"
"github.com/ghodss/yaml" "github.com/ghodss/yaml"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/xeipuuv/gojsonschema"
"helm.sh/helm/pkg/chart" "helm.sh/helm/pkg/chart"
) )
// ErrNoTable indicates that a chart does not have a matching table.
type ErrNoTable string
func (e ErrNoTable) Error() string { return fmt.Sprintf("%q is not a table", e) }
// ErrNoValue indicates that Values does not contain a key with a value
type ErrNoValue string
func (e ErrNoValue) Error() string { return fmt.Sprintf("%q is not a value", e) }
// GlobalKey is the name of the Values key that is used for storing global vars. // GlobalKey is the name of the Values key that is used for storing global vars.
const GlobalKey = "global" const GlobalKey = "global"
@ -89,7 +76,6 @@ func (v Values) AsMap() map[string]interface{} {
// Encode writes serialized Values information to the given io.Writer. // Encode writes serialized Values information to the given io.Writer.
func (v Values) Encode(w io.Writer) error { func (v Values) Encode(w io.Writer) error {
//return yaml.NewEncoder(w).Encode(v)
out, err := yaml.Marshal(v) out, err := yaml.Marshal(v)
if err != nil { if err != nil {
return err return err
@ -135,234 +121,6 @@ func ReadValuesFile(filename string) (Values, error) {
return ReadValues(data) return ReadValues(data)
} }
// ValidateAgainstSchema checks that values does not violate the structure laid out in schema
func ValidateAgainstSchema(chrt *chart.Chart, values map[string]interface{}) error {
var sb strings.Builder
if chrt.Schema != nil {
err := ValidateAgainstSingleSchema(values, chrt.Schema)
if err != nil {
sb.WriteString(fmt.Sprintf("%s:\n", chrt.Name()))
sb.WriteString(err.Error())
}
}
// For each dependency, recurively call this function with the coalesced values
for _, subchrt := range chrt.Dependencies() {
subchrtValues := values[subchrt.Name()].(map[string]interface{})
if err := ValidateAgainstSchema(subchrt, subchrtValues); err != nil {
sb.WriteString(err.Error())
}
}
if sb.Len() > 0 {
return errors.New(sb.String())
}
return nil
}
// ValidateAgainstSingleSchema checks that values does not violate the structure laid out in this schema
func ValidateAgainstSingleSchema(values Values, schemaJSON []byte) error {
valuesData, err := yaml.Marshal(values)
if err != nil {
return err
}
valuesJSON, err := yaml.YAMLToJSON(valuesData)
if err != nil {
return err
}
if bytes.Equal(valuesJSON, []byte("null")) {
valuesJSON = []byte("{}")
}
schemaLoader := gojsonschema.NewBytesLoader(schemaJSON)
valuesLoader := gojsonschema.NewBytesLoader(valuesJSON)
result, err := gojsonschema.Validate(schemaLoader, valuesLoader)
if err != nil {
return err
}
if !result.Valid() {
var sb strings.Builder
for _, desc := range result.Errors() {
sb.WriteString(fmt.Sprintf("- %s\n", desc))
}
return errors.New(sb.String())
}
return nil
}
// CoalesceValues coalesces all of the values in a chart (and its subcharts).
//
// Values are coalesced together using the following rules:
//
// - Values in a higher level chart always override values in a lower-level
// dependency chart
// - Scalar values and arrays are replaced, maps are merged
// - A chart has access to all of the variables for it, as well as all of
// the values destined for its dependencies.
func CoalesceValues(chrt *chart.Chart, vals map[string]interface{}) (Values, error) {
if vals == nil {
vals = make(map[string]interface{})
}
if _, err := coalesce(chrt, vals); err != nil {
return vals, err
}
return coalesceDeps(chrt, vals)
}
// coalesce coalesces the dest values and the chart values, giving priority to the dest values.
//
// This is a helper function for CoalesceValues.
func coalesce(ch *chart.Chart, dest map[string]interface{}) (map[string]interface{}, error) {
coalesceValues(ch, dest)
return coalesceDeps(ch, dest)
}
// coalesceDeps coalesces the dependencies of the given chart.
func coalesceDeps(chrt *chart.Chart, dest map[string]interface{}) (map[string]interface{}, error) {
for _, subchart := range chrt.Dependencies() {
if c, ok := dest[subchart.Name()]; !ok {
// If dest doesn't already have the key, create it.
dest[subchart.Name()] = make(map[string]interface{})
} else if !istable(c) {
return dest, errors.Errorf("type mismatch on %s: %t", subchart.Name(), c)
}
if dv, ok := dest[subchart.Name()]; ok {
dvmap := dv.(map[string]interface{})
// Get globals out of dest and merge them into dvmap.
coalesceGlobals(dvmap, dest)
// Now coalesce the rest of the values.
var err error
dest[subchart.Name()], err = coalesce(subchart, dvmap)
if err != nil {
return dest, err
}
}
}
return dest, nil
}
// coalesceGlobals copies the globals out of src and merges them into dest.
//
// For convenience, returns dest.
func coalesceGlobals(dest, src map[string]interface{}) {
var dg, sg map[string]interface{}
if destglob, ok := dest[GlobalKey]; !ok {
dg = make(map[string]interface{})
} else if dg, ok = destglob.(map[string]interface{}); !ok {
log.Printf("warning: skipping globals because destination %s is not a table.", GlobalKey)
return
}
if srcglob, ok := src[GlobalKey]; !ok {
sg = make(map[string]interface{})
} else if sg, ok = srcglob.(map[string]interface{}); !ok {
log.Printf("warning: skipping globals because source %s is not a table.", GlobalKey)
return
}
// EXPERIMENTAL: In the past, we have disallowed globals to test tables. This
// reverses that decision. It may somehow be possible to introduce a loop
// here, but I haven't found a way. So for the time being, let's allow
// tables in globals.
for key, val := range sg {
if istable(val) {
vv := copyMap(val.(map[string]interface{}))
if destv, ok := dg[key]; !ok {
// Here there is no merge. We're just adding.
dg[key] = vv
} else {
if destvmap, ok := destv.(map[string]interface{}); !ok {
log.Printf("Conflict: cannot merge map onto non-map for %q. Skipping.", key)
} else {
// Basically, we reverse order of coalesce here to merge
// top-down.
CoalesceTables(vv, destvmap)
dg[key] = vv
continue
}
}
} else if dv, ok := dg[key]; ok && istable(dv) {
// It's not clear if this condition can actually ever trigger.
log.Printf("key %s is table. Skipping", key)
continue
}
// TODO: Do we need to do any additional checking on the value?
dg[key] = val
}
dest[GlobalKey] = dg
}
func copyMap(src map[string]interface{}) map[string]interface{} {
m := make(map[string]interface{}, len(src))
for k, v := range src {
m[k] = v
}
return m
}
// coalesceValues builds up a values map for a particular chart.
//
// Values in v will override the values in the chart.
func coalesceValues(c *chart.Chart, v map[string]interface{}) {
for key, val := range c.Values {
if value, ok := v[key]; ok {
if value == nil {
// When the YAML value is null, we remove the value's key.
// This allows Helm's various sources of values (value files or --set) to
// remove incompatible keys from any previous chart, file, or set values.
delete(v, key)
} else if dest, ok := value.(map[string]interface{}); ok {
// if v[key] is a table, merge nv's val table into v[key].
src, ok := val.(map[string]interface{})
if !ok {
log.Printf("warning: skipped value for %s: Not a table.", key)
continue
}
// Because v has higher precedence than nv, dest values override src
// values.
CoalesceTables(dest, src)
}
} else {
// If the key is not in v, copy it from nv.
v[key] = val
}
}
}
// CoalesceTables merges a source map into a destination map.
//
// dest is considered authoritative.
func CoalesceTables(dst, src map[string]interface{}) map[string]interface{} {
if dst == nil || src == nil {
return src
}
// Because dest has higher precedence than src, dest values override src
// values.
for key, val := range src {
if istable(val) {
switch innerdst, ok := dst[key]; {
case !ok:
dst[key] = val
case istable(innerdst):
CoalesceTables(innerdst.(map[string]interface{}), val.(map[string]interface{}))
default:
log.Printf("warning: cannot overwrite table with non table for %s (%v)", key, val)
}
} else if dv, ok := dst[key]; ok && istable(dv) {
log.Printf("warning: destination for %s is a table. Ignoring non-table value %v", key, val)
} else if !ok { // <- ok is still in scope from preceding conditional.
dst[key] = val
}
}
return dst
}
// ReleaseOptions represents the additional release options needed // ReleaseOptions represents the additional release options needed
// for the composition of the final values struct // for the composition of the final values struct
type ReleaseOptions struct { type ReleaseOptions struct {

@ -18,9 +18,7 @@ package chartutil
import ( import (
"bytes" "bytes"
"encoding/json"
"fmt" "fmt"
"io/ioutil"
"testing" "testing"
"text/template" "text/template"
@ -154,125 +152,6 @@ func TestReadValuesFile(t *testing.T) {
matchValues(t, data) matchValues(t, data)
} }
func TestValidateAgainstSingleSchema(t *testing.T) {
values, err := ReadValuesFile("./testdata/test-values.yaml")
if err != nil {
t.Fatalf("Error reading YAML file: %s", err)
}
schema, err := ioutil.ReadFile("./testdata/test-values.schema.json")
if err != nil {
t.Fatalf("Error reading YAML file: %s", err)
}
if err := ValidateAgainstSingleSchema(values, schema); err != nil {
t.Errorf("Error validating Values against Schema: %s", err)
}
}
func TestValidateAgainstSingleSchemaNegative(t *testing.T) {
values, err := ReadValuesFile("./testdata/test-values-negative.yaml")
if err != nil {
t.Fatalf("Error reading YAML file: %s", err)
}
schema, err := ioutil.ReadFile("./testdata/test-values.schema.json")
if err != nil {
t.Fatalf("Error reading YAML file: %s", err)
}
var errString string
if err := ValidateAgainstSingleSchema(values, schema); err == nil {
t.Fatalf("Expected an error, but got nil")
} else {
errString = err.Error()
}
expectedErrString := `- (root): employmentInfo is required
- age: Must be greater than or equal to 0/1
`
if errString != expectedErrString {
t.Errorf("Error string :\n`%s`\ndoes not match expected\n`%s`", errString, expectedErrString)
}
}
const subchrtSchema = `{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Values",
"type": "object",
"properties": {
"age": {
"description": "Age",
"minimum": 0,
"type": "integer"
}
},
"required": [
"age"
]
}
`
func TestValidateAgainstSchema(t *testing.T) {
subchrtJSON := []byte(subchrtSchema)
subchrt := &chart.Chart{
Metadata: &chart.Metadata{
Name: "subchrt",
},
Schema: subchrtJSON,
}
chrt := &chart.Chart{
Metadata: &chart.Metadata{
Name: "chrt",
},
}
chrt.AddDependency(subchrt)
vals := map[string]interface{}{
"name": "John",
"subchrt": map[string]interface{}{
"age": 25,
},
}
if err := ValidateAgainstSchema(chrt, vals); err != nil {
t.Errorf("Error validating Values against Schema: %s", err)
}
}
func TestValidateAgainstSchemaNegative(t *testing.T) {
subchrtJSON := []byte(subchrtSchema)
subchrt := &chart.Chart{
Metadata: &chart.Metadata{
Name: "subchrt",
},
Schema: subchrtJSON,
}
chrt := &chart.Chart{
Metadata: &chart.Metadata{
Name: "chrt",
},
}
chrt.AddDependency(subchrt)
vals := map[string]interface{}{
"name": "John",
"subchrt": map[string]interface{}{},
}
var errString string
if err := ValidateAgainstSchema(chrt, vals); err == nil {
t.Fatalf("Expected an error, but got nil")
} else {
errString = err.Error()
}
expectedErrString := `subchrt:
- (root): age is required
`
if errString != expectedErrString {
t.Errorf("Error string :\n`%s`\ndoes not match expected\n`%s`", errString, expectedErrString)
}
}
func ExampleValues() { func ExampleValues() {
doc := ` doc := `
title: "Moby Dick" title: "Moby Dick"
@ -367,152 +246,6 @@ func ttpl(tpl string, v map[string]interface{}) (string, error) {
return b.String(), err return b.String(), err
} }
// ref: http://www.yaml.org/spec/1.2/spec.html#id2803362
var testCoalesceValuesYaml = []byte(`
top: yup
bottom: null
right: Null
left: NULL
front: ~
back: ""
global:
name: Ishmael
subject: Queequeg
nested:
boat: true
pequod:
global:
name: Stinky
harpooner: Tashtego
nested:
boat: false
sail: true
ahab:
scope: whale
`)
func TestCoalesceValues(t *testing.T) {
c := loadChart(t, "testdata/moby")
vals, err := ReadValues(testCoalesceValuesYaml)
if err != nil {
t.Fatal(err)
}
v, err := CoalesceValues(c, vals)
if err != nil {
t.Fatal(err)
}
j, _ := json.MarshalIndent(v, "", " ")
t.Logf("Coalesced Values: %s", string(j))
tests := []struct {
tpl string
expect string
}{
{"{{.top}}", "yup"},
{"{{.back}}", ""},
{"{{.name}}", "moby"},
{"{{.global.name}}", "Ishmael"},
{"{{.global.subject}}", "Queequeg"},
{"{{.global.harpooner}}", "<no value>"},
{"{{.pequod.name}}", "pequod"},
{"{{.pequod.ahab.name}}", "ahab"},
{"{{.pequod.ahab.scope}}", "whale"},
{"{{.pequod.ahab.global.name}}", "Ishmael"},
{"{{.pequod.ahab.global.subject}}", "Queequeg"},
{"{{.pequod.ahab.global.harpooner}}", "Tashtego"},
{"{{.pequod.global.name}}", "Ishmael"},
{"{{.pequod.global.subject}}", "Queequeg"},
{"{{.spouter.global.name}}", "Ishmael"},
{"{{.spouter.global.harpooner}}", "<no value>"},
{"{{.global.nested.boat}}", "true"},
{"{{.pequod.global.nested.boat}}", "true"},
{"{{.spouter.global.nested.boat}}", "true"},
{"{{.pequod.global.nested.sail}}", "true"},
{"{{.spouter.global.nested.sail}}", "<no value>"},
}
for _, tt := range tests {
if o, err := ttpl(tt.tpl, v); err != nil || o != tt.expect {
t.Errorf("Expected %q to expand to %q, got %q", tt.tpl, tt.expect, o)
}
}
nullKeys := []string{"bottom", "right", "left", "front"}
for _, nullKey := range nullKeys {
if _, ok := v[nullKey]; ok {
t.Errorf("Expected key %q to be removed, still present", nullKey)
}
}
}
func TestCoalesceTables(t *testing.T) {
dst := map[string]interface{}{
"name": "Ishmael",
"address": map[string]interface{}{
"street": "123 Spouter Inn Ct.",
"city": "Nantucket",
},
"details": map[string]interface{}{
"friends": []string{"Tashtego"},
},
"boat": "pequod",
}
src := map[string]interface{}{
"occupation": "whaler",
"address": map[string]interface{}{
"state": "MA",
"street": "234 Spouter Inn Ct.",
},
"details": "empty",
"boat": map[string]interface{}{
"mast": true,
},
}
// What we expect is that anything in dst overrides anything in src, but that
// otherwise the values are coalesced.
CoalesceTables(dst, src)
if dst["name"] != "Ishmael" {
t.Errorf("Unexpected name: %s", dst["name"])
}
if dst["occupation"] != "whaler" {
t.Errorf("Unexpected occupation: %s", dst["occupation"])
}
addr, ok := dst["address"].(map[string]interface{})
if !ok {
t.Fatal("Address went away.")
}
if addr["street"].(string) != "123 Spouter Inn Ct." {
t.Errorf("Unexpected address: %v", addr["street"])
}
if addr["city"].(string) != "Nantucket" {
t.Errorf("Unexpected city: %v", addr["city"])
}
if addr["state"].(string) != "MA" {
t.Errorf("Unexpected state: %v", addr["state"])
}
if det, ok := dst["details"].(map[string]interface{}); !ok {
t.Fatalf("Details is the wrong type: %v", dst["details"])
} else if _, ok := det["friends"]; !ok {
t.Error("Could not find your friends. Maybe you don't have any. :-(")
}
if dst["boat"].(string) != "pequod" {
t.Errorf("Expected boat string, got %v", dst["boat"])
}
}
func TestPathValue(t *testing.T) { func TestPathValue(t *testing.T) {
doc := ` doc := `
title: "Moby Dick" title: "Moby Dick"

@ -19,6 +19,7 @@ package engine
import ( import (
"fmt" "fmt"
"path" "path"
"path/filepath"
"sort" "sort"
"strings" "strings"
"text/template" "text/template"
@ -268,10 +269,9 @@ func recAllTpls(c *chart.Chart, templates map[string]renderable, vals chartutil.
recAllTpls(child, templates, next) recAllTpls(child, templates, next)
} }
isLibChart := chartutil.IsLibraryChart(c)
newParentID := c.ChartFullPath() newParentID := c.ChartFullPath()
for _, t := range c.Templates { for _, t := range c.Templates {
if !chartutil.IsTemplateValid(t.Name, isLibChart) { if !isTemplateValid(c, t.Name) {
continue continue
} }
templates[path.Join(newParentID, t.Name)] = renderable{ templates[path.Join(newParentID, t.Name)] = renderable{
@ -281,3 +281,16 @@ func recAllTpls(c *chart.Chart, templates map[string]renderable, vals chartutil.
} }
} }
} }
// isTemplateValid returns true if the template is valid for the chart type
func isTemplateValid(ch *chart.Chart, templateName string) bool {
if isLibraryChart(ch) {
return strings.HasPrefix(filepath.Base(templateName), "_")
}
return true
}
// isLibraryChart returns true if the chart is a library chart
func isLibraryChart(c *chart.Chart) bool {
return strings.EqualFold(c.Metadata.Type, "library")
}

Loading…
Cancel
Save