Signed-off-by: Aurélien Lambert <aure@olli-ai.com>
pull/6876/head
Aurélien Lambert 6 years ago
commit b143f2b89e
No known key found for this signature in database
GPG Key ID: 676BC3D8C0C1071B

@ -17,6 +17,7 @@ linters:
- structcheck
- unused
- varcheck
- staticcheck
linters-settings:
gofmt:

@ -1,12 +1,15 @@
To add your organization to this list, simply add your organization's name,
optionally with a link. The list is in alphabetical order.
To add your organization to this list, open a pull request that adds your
organization's name, optionally with a link. The list is in alphabetical order.
(Remember to use `git commit --signoff` to comply with the DCO)
# Organizations Using Helm
- [Blood Orange](https://bloodorange.io)
- [Microsoft](https://microsoft.com)
- [IBM](https://www.ibm.com)
- [Microsoft](https://microsoft.com)
- [Qovery](https://www.qovery.com/)
- [Samsung SDS](https://www.samsungsds.com/)
[Ville de Montreal](https://montreal.ca)
- [Ville de Montreal](https://montreal.ca)
_This file is part of the CNCF official documentation for projects._

@ -1,7 +1,7 @@
BINDIR := $(CURDIR)/bin
DIST_DIRS := find * -type d -exec
TARGETS := darwin/amd64 linux/amd64 linux/386 linux/arm linux/arm64 linux/ppc64le linux/s390x windows/amd64
TARGET_OBJS ?= darwin-amd64.tar.gz darwin-amd64.tar.gz.sha256 linux-amd64.tar.gz linux-amd64.tar.gz.sha256 linux-386.tar.gz linux-386.tar.gz.sha256 linux-arm.tar.gz linux-arm.tar.gz.sha256 linux-arm64.tar.gz linux-arm64.tar.gz.sha256 linux-ppc64le.tar.gz linux-ppc64le.tar.gz.sha256 linux-s390x.tar.gz linux-s390x.tar.gz.sha256 windows-amd64.zip windows-amd64.zip.sha256
TARGET_OBJS ?= darwin-amd64.tar.gz darwin-amd64.tar.gz.sha256 darwin-amd64.tar.gz.sha256sum linux-amd64.tar.gz linux-amd64.tar.gz.sha256 linux-amd64.tar.gz.sha256sum linux-386.tar.gz linux-386.tar.gz.sha256 linux-386.tar.gz.sha256sum linux-arm.tar.gz linux-arm.tar.gz.sha256 linux-arm.tar.gz.sha256sum linux-arm64.tar.gz linux-arm64.tar.gz.sha256 linux-arm64.tar.gz.sha256sum linux-ppc64le.tar.gz linux-ppc64le.tar.gz.sha256 linux-ppc64le.tar.gz.sha256sum linux-s390x.tar.gz linux-s390x.tar.gz.sha256 linux-s390x.tar.gz.sha256sum windows-amd64.zip windows-amd64.zip.sha256 windows-amd64.zip.sha256sum
BINNAME ?= helm
GOPATH = $(shell go env GOPATH)
@ -24,7 +24,7 @@ GOFLAGS :=
SRC := $(shell find . -type f -name '*.go' -print)
# Required for globs to work correctly
SHELL = /bin/bash
SHELL = /usr/bin/env bash
GIT_COMMIT = $(shell git rev-parse HEAD)
GIT_SHA = $(shell git rev-parse --short HEAD)
@ -181,6 +181,21 @@ checksum:
clean:
@rm -rf $(BINDIR) ./_dist
.PHONY: release-notes
release-notes:
@if [ ! -d "./_dist" ]; then \
echo "please run 'make fetch-release' first" && \
exit 1; \
fi
@if [ -z "${PREVIOUS_RELEASE}" ]; then \
echo "please set PREVIOUS_RELEASE environment variable" \
&& exit 1; \
fi
@./scripts/release-notes.sh ${PREVIOUS_RELEASE} ${VERSION}
.PHONY: info
info:
@echo "Version: ${VERSION}"

@ -19,6 +19,7 @@ package main // import "helm.sh/helm/v3/cmd/helm"
import (
"flag"
"fmt"
"io/ioutil"
"log"
"os"
"strings"
@ -26,13 +27,18 @@ import (
"github.com/spf13/cobra"
"github.com/spf13/pflag"
"k8s.io/klog"
"sigs.k8s.io/yaml"
// Import to initialize client auth plugins.
_ "k8s.io/client-go/plugin/pkg/client/auth"
"helm.sh/helm/v3/internal/completion"
"helm.sh/helm/v3/pkg/action"
"helm.sh/helm/v3/pkg/cli"
"helm.sh/helm/v3/pkg/gates"
kubefake "helm.sh/helm/v3/pkg/kube/fake"
"helm.sh/helm/v3/pkg/release"
"helm.sh/helm/v3/pkg/storage/driver"
)
// FeatureGateOCI is the feature gate for checking if `helm chart` and `helm registry` commands should work
@ -67,9 +73,23 @@ func main() {
actionConfig := new(action.Configuration)
cmd := newRootCmd(actionConfig, os.Stdout, os.Args[1:])
if err := actionConfig.Init(settings.RESTClientGetter(), settings.Namespace(), os.Getenv("HELM_DRIVER"), debug); err != nil {
if calledCmd, _, err := cmd.Find(os.Args[1:]); err == nil && calledCmd.Name() == completion.CompRequestCmd {
// If completion is being called, we have to check if the completion is for the "--kube-context"
// value; if it is, we cannot call the action.Init() method with an incomplete kube-context value
// or else it will fail immediately. So, we simply unset the invalid kube-context value.
if args := os.Args[1:]; len(args) > 2 && args[len(args)-2] == "--kube-context" {
// We are completing the kube-context value! Reset it as the current value is not valid.
settings.KubeContext = ""
}
}
helmDriver := os.Getenv("HELM_DRIVER")
if err := actionConfig.Init(settings.RESTClientGetter(), settings.Namespace(), helmDriver, debug); err != nil {
log.Fatal(err)
}
if helmDriver == "memory" {
loadReleasesInMemory(actionConfig)
}
if err := cmd.Execute(); err != nil {
debug("%+v", err)
@ -95,3 +115,41 @@ func checkOCIFeatureGate() func(_ *cobra.Command, _ []string) error {
return nil
}
}
// This function loads releases into the memory storage if the
// environment variable is properly set.
func loadReleasesInMemory(actionConfig *action.Configuration) {
filePaths := strings.Split(os.Getenv("HELM_MEMORY_DRIVER_DATA"), ":")
if len(filePaths) == 0 {
return
}
store := actionConfig.Releases
mem, ok := store.Driver.(*driver.Memory)
if !ok {
// For an unexpected reason we are not dealing with the memory storage driver.
return
}
actionConfig.KubeClient = &kubefake.PrintingKubeClient{Out: ioutil.Discard}
for _, path := range filePaths {
b, err := ioutil.ReadFile(path)
if err != nil {
log.Fatal("Unable to read memory driver data", err)
}
releases := []*release.Release{}
if err := yaml.Unmarshal(b, &releases); err != nil {
log.Fatal("Unable to unmarshal memory driver data: ", err)
}
for _, rel := range releases {
if err := store.Create(rel); err != nil {
log.Fatal(err)
}
}
}
// Must reset namespace to the proper one
mem.SetNamespace(settings.Namespace())
}

@ -18,7 +18,6 @@ package main
import (
"bytes"
"fmt"
"io/ioutil"
"os"
"os/exec"
@ -97,39 +96,11 @@ func storageFixture() *storage.Storage {
return storage.Init(driver.NewMemory())
}
// go-shellwords does not handle empty arguments properly
// https://github.com/mattn/go-shellwords/issues/5#issuecomment-573431458
//
// This method checks if the last argument was an empty one,
// and if go-shellwords missed it, we add it ourselves.
//
// This is important for completion tests as completion often
// uses an empty last parameter.
func checkLastEmpty(in string, out []string) []string {
lastIndex := len(in) - 1
if lastIndex >= 1 && (in[lastIndex] == '"' && in[lastIndex-1] == '"' ||
in[lastIndex] == '\'' && in[lastIndex-1] == '\'') {
// The last parameter of 'in' was empty ("" or ''), let's make sure it was detected.
if len(out) > 0 && out[len(out)-1] != "" {
// Bug from go-shellwords:
// 'out' does not have the empty parameter. We add it ourselves as a workaround.
out = append(out, "")
} else {
fmt.Println("WARNING: go-shellwords seems to have been fixed. This workaround can be removed.")
}
}
return out
}
func executeActionCommandC(store *storage.Storage, cmd string) (*cobra.Command, string, error) {
args, err := shellwords.Parse(cmd)
if err != nil {
return nil, "", err
}
// Workaround the bug in shellwords
args = checkLastEmpty(cmd, args)
buf := new(bytes.Buffer)
actionConfig := &action.Configuration{

@ -136,6 +136,7 @@ func newInstallCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
}
func addInstallFlags(f *pflag.FlagSet, client *action.Install, valueOpts *values.Options) {
f.BoolVar(&client.CreateNamespace, "create-namespace", false, "create the release namespace if not present")
f.BoolVar(&client.DryRun, "dry-run", false, "simulate an install")
f.BoolVar(&client.DisableHooks, "no-hooks", false, "prevent hooks from running during install")
f.BoolVar(&client.Replace, "replace", false, "re-use the given name, only if that name is a deleted release which remains in the history. This is unsafe in production")

@ -106,7 +106,7 @@ func newLintCmd(out io.Writer) *cobra.Command {
fmt.Fprint(&message, "\n")
}
fmt.Fprintf(out, message.String())
fmt.Fprint(out, message.String())
summary := fmt.Sprintf("%d chart(s) linted, %d chart(s) failed", len(paths), failed)
if failed > 0 {

@ -16,20 +16,31 @@ limitations under the License.
package main
import (
"bytes"
"fmt"
"io"
"io/ioutil"
"log"
"os"
"os/exec"
"path/filepath"
"strconv"
"strings"
"syscall"
"github.com/pkg/errors"
"github.com/spf13/cobra"
"sigs.k8s.io/yaml"
"helm.sh/helm/v3/internal/completion"
"helm.sh/helm/v3/pkg/plugin"
)
const (
pluginStaticCompletionFile = "completion.yaml"
pluginDynamicCompletionExecutable = "plugin.complete"
)
type pluginError struct {
error
code int
@ -61,6 +72,13 @@ func loadPlugins(baseCmd *cobra.Command, out io.Writer) {
return u, nil
}
// If we are dealing with the completion command, we try to load more details about the plugins
// if available, so as to allow for command and flag completion
if subCmd, _, err := baseCmd.Find(os.Args[1:]); err == nil && subCmd.Name() == "completion" {
loadPluginsForCompletion(baseCmd, found)
return
}
// Now we create commands for all of these.
for _, plug := range found {
plug := plug
@ -69,26 +87,9 @@ func loadPlugins(baseCmd *cobra.Command, out io.Writer) {
md.Usage = fmt.Sprintf("the %q plugin", md.Name)
}
c := &cobra.Command{
Use: md.Name,
Short: md.Usage,
Long: md.Description,
RunE: func(cmd *cobra.Command, args []string) error {
u, err := processParent(cmd, args)
if err != nil {
return err
}
// Call setupEnv before PrepareCommand because
// PrepareCommand uses os.ExpandEnv and expects the
// setupEnv vars.
plugin.SetupPluginEnv(settings, md.Name, plug.Dir)
main, argv, prepCmdErr := plug.PrepareCommand(u)
if prepCmdErr != nil {
os.Stderr.WriteString(prepCmdErr.Error())
return errors.Errorf("plugin %q exited with error", md.Name)
}
// This function is used to setup the environment for the plugin and then
// call the executable specified by the parameter 'main'
callPluginExecutable := func(cmd *cobra.Command, main string, argv []string, out io.Writer) error {
env := os.Environ()
for k, v := range settings.EnvVars() {
env = append(env, fmt.Sprintf("%s=%s", k, v))
@ -111,11 +112,81 @@ func loadPlugins(baseCmd *cobra.Command, out io.Writer) {
return err
}
return nil
}
c := &cobra.Command{
Use: md.Name,
Short: md.Usage,
Long: md.Description,
RunE: func(cmd *cobra.Command, args []string) error {
u, err := processParent(cmd, args)
if err != nil {
return err
}
// Call setupEnv before PrepareCommand because
// PrepareCommand uses os.ExpandEnv and expects the
// setupEnv vars.
plugin.SetupPluginEnv(settings, md.Name, plug.Dir)
main, argv, prepCmdErr := plug.PrepareCommand(u)
if prepCmdErr != nil {
os.Stderr.WriteString(prepCmdErr.Error())
return errors.Errorf("plugin %q exited with error", md.Name)
}
return callPluginExecutable(cmd, main, argv, out)
},
// This passes all the flags to the subcommand.
DisableFlagParsing: true,
}
// Setup dynamic completion for the plugin
completion.RegisterValidArgsFunc(c, func(cmd *cobra.Command, args []string, toComplete string) ([]string, completion.BashCompDirective) {
u, err := processParent(cmd, args)
if err != nil {
return nil, completion.BashCompDirectiveError
}
// We will call the dynamic completion script of the plugin
main := strings.Join([]string{plug.Dir, pluginDynamicCompletionExecutable}, string(filepath.Separator))
argv := []string{}
if !md.IgnoreFlags {
argv = append(argv, u...)
argv = append(argv, toComplete)
}
plugin.SetupPluginEnv(settings, md.Name, plug.Dir)
completion.CompDebugln(fmt.Sprintf("calling %s with args %v", main, argv))
buf := new(bytes.Buffer)
if err := callPluginExecutable(cmd, main, argv, buf); err != nil {
return nil, completion.BashCompDirectiveError
}
var completions []string
for _, comp := range strings.Split(buf.String(), "\n") {
// Remove any empty lines
if len(comp) > 0 {
completions = append(completions, comp)
}
}
// Check if the last line of output is of the form :<integer>, which
// indicates the BashCompletionDirective.
directive := completion.BashCompDirectiveDefault
if len(completions) > 0 {
lastLine := completions[len(completions)-1]
if len(lastLine) > 1 && lastLine[0] == ':' {
if strInt, err := strconv.Atoi(lastLine[1:]); err == nil {
directive = completion.BashCompDirective(strInt)
completions = completions[:len(completions)-1]
}
}
}
return completions, directive
})
// TODO: Make sure a command with this name does not already exist.
baseCmd.AddCommand(c)
}
@ -127,7 +198,7 @@ func loadPlugins(baseCmd *cobra.Command, out io.Writer) {
func manuallyProcessArgs(args []string) ([]string, []string) {
known := []string{}
unknown := []string{}
kvargs := []string{"--kube-context", "--namespace", "-n", "--kubeconfig", "--registry-config", "--repository-cache", "--repository-config"}
kvargs := []string{"--kube-context", "--namespace", "-n", "--kubeconfig", "--kube-apiserver", "--kube-token", "--registry-config", "--repository-cache", "--repository-config"}
knownArg := func(a string) bool {
for _, pre := range kvargs {
if strings.HasPrefix(a, pre+"=") {
@ -180,3 +251,119 @@ func findPlugins(plugdirs string) ([]*plugin.Plugin, error) {
}
return found, nil
}
// pluginCommand represents the optional completion.yaml file of a plugin
type pluginCommand struct {
Name string `json:"name"`
ValidArgs []string `json:"validArgs"`
Flags []string `json:"flags"`
Commands []pluginCommand `json:"commands"`
}
// loadPluginsForCompletion will load and parse any completion.yaml provided by the plugins
func loadPluginsForCompletion(baseCmd *cobra.Command, plugins []*plugin.Plugin) {
for _, plug := range plugins {
// Parse the yaml file providing the plugin's subcmds and flags
cmds, err := loadFile(strings.Join(
[]string{plug.Dir, pluginStaticCompletionFile}, string(filepath.Separator)))
if err != nil {
// The file could be missing or invalid. Either way, we at least create the command
// for the plugin name.
if settings.Debug {
log.Output(2, fmt.Sprintf("[info] %s\n", err.Error()))
}
cmds = &pluginCommand{Name: plug.Metadata.Name}
}
// We know what the plugin name must be.
// Let's set it in case the Name field was not specified correctly in the file.
// This insures that we will at least get the plugin name to complete, even if
// there is a problem with the completion.yaml file
cmds.Name = plug.Metadata.Name
addPluginCommands(baseCmd, cmds)
}
}
// addPluginCommands is a recursive method that adds the different levels
// of sub-commands and flags for the plugins that provide such information
func addPluginCommands(baseCmd *cobra.Command, cmds *pluginCommand) {
if cmds == nil {
return
}
if len(cmds.Name) == 0 {
// Missing name for a command
if settings.Debug {
log.Output(2, fmt.Sprintf("[info] sub-command name field missing for %s", baseCmd.CommandPath()))
}
return
}
// Create a fake command just so the completion script will include it
c := &cobra.Command{
Use: cmds.Name,
ValidArgs: cmds.ValidArgs,
// A Run is required for it to be a valid command without subcommands
Run: func(cmd *cobra.Command, args []string) {},
}
baseCmd.AddCommand(c)
// Create fake flags.
if len(cmds.Flags) > 0 {
// The flags can be created with any type, since we only need them for completion.
// pflag does not allow to create short flags without a corresponding long form
// so we look for all short flags and match them to any long flag. This will allow
// plugins to provide short flags without a long form.
// If there are more short-flags than long ones, we'll create an extra long flag with
// the same single letter as the short form.
shorts := []string{}
longs := []string{}
for _, flag := range cmds.Flags {
if len(flag) == 1 {
shorts = append(shorts, flag)
} else {
longs = append(longs, flag)
}
}
f := c.Flags()
if len(longs) >= len(shorts) {
for i := range longs {
if i < len(shorts) {
f.BoolP(longs[i], shorts[i], false, "")
} else {
f.Bool(longs[i], false, "")
}
}
} else {
for i := range shorts {
if i < len(longs) {
f.BoolP(longs[i], shorts[i], false, "")
} else {
// Create a long flag with the same name as the short flag.
// Not a perfect solution, but its better than ignoring the extra short flags.
f.BoolP(shorts[i], shorts[i], false, "")
}
}
}
}
// Recursively add any sub-commands
for _, cmd := range cmds.Commands {
addPluginCommands(c, &cmd)
}
}
// loadFile takes a yaml file at the given path, parses it and returns a pluginCommand object
func loadFile(path string) (*pluginCommand, error) {
cmds := new(pluginCommand)
b, err := ioutil.ReadFile(path)
if err != nil {
return cmds, errors.New(fmt.Sprintf("File (%s) not provided by plugin. No plugin auto-completion possible.", path))
}
err = yaml.Unmarshal(b, cmds)
return cmds, err
}

@ -19,10 +19,14 @@ import (
"bytes"
"os"
"runtime"
"sort"
"strings"
"testing"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
"helm.sh/helm/v3/pkg/release"
)
func TestManuallyProcessArgs(t *testing.T) {
@ -151,6 +155,134 @@ func TestLoadPlugins(t *testing.T) {
}
}
type staticCompletionDetails struct {
use string
validArgs []string
flags []string
next []staticCompletionDetails
}
func TestLoadPluginsForCompletion(t *testing.T) {
settings.PluginsDirectory = "testdata/helmhome/helm/plugins"
var out bytes.Buffer
cmd := &cobra.Command{
Use: "completion",
}
loadPlugins(cmd, &out)
tests := []staticCompletionDetails{
{"args", []string{}, []string{}, []staticCompletionDetails{}},
{"echo", []string{}, []string{}, []staticCompletionDetails{}},
{"env", []string{}, []string{"global"}, []staticCompletionDetails{
{"list", []string{}, []string{"a", "all", "log"}, []staticCompletionDetails{}},
{"remove", []string{"all", "one"}, []string{}, []staticCompletionDetails{}},
}},
{"exitwith", []string{}, []string{}, []staticCompletionDetails{
{"code", []string{}, []string{"a", "b"}, []staticCompletionDetails{}},
}},
{"fullenv", []string{}, []string{"q", "z"}, []staticCompletionDetails{
{"empty", []string{}, []string{}, []staticCompletionDetails{}},
{"full", []string{}, []string{}, []staticCompletionDetails{
{"less", []string{}, []string{"a", "all"}, []staticCompletionDetails{}},
{"more", []string{"one", "two"}, []string{"b", "ball"}, []staticCompletionDetails{}},
}},
}},
}
checkCommand(t, cmd.Commands(), tests)
}
func checkCommand(t *testing.T, plugins []*cobra.Command, tests []staticCompletionDetails) {
if len(plugins) != len(tests) {
t.Fatalf("Expected commands %v, got %v", tests, plugins)
}
for i := 0; i < len(plugins); i++ {
pp := plugins[i]
tt := tests[i]
if pp.Use != tt.use {
t.Errorf("%s: Expected Use=%q, got %q", pp.Name(), tt.use, pp.Use)
}
targs := tt.validArgs
pargs := pp.ValidArgs
if len(targs) != len(pargs) {
t.Fatalf("%s: expected args %v, got %v", pp.Name(), targs, pargs)
}
sort.Strings(targs)
sort.Strings(pargs)
for j := range targs {
if targs[j] != pargs[j] {
t.Errorf("%s: expected validArg=%q, got %q", pp.Name(), targs[j], pargs[j])
}
}
tflags := tt.flags
var pflags []string
pp.LocalFlags().VisitAll(func(flag *pflag.Flag) {
pflags = append(pflags, flag.Name)
if len(flag.Shorthand) > 0 && flag.Shorthand != flag.Name {
pflags = append(pflags, flag.Shorthand)
}
})
if len(tflags) != len(pflags) {
t.Fatalf("%s: expected flags %v, got %v", pp.Name(), tflags, pflags)
}
sort.Strings(tflags)
sort.Strings(pflags)
for j := range tflags {
if tflags[j] != pflags[j] {
t.Errorf("%s: expected flag=%q, got %q", pp.Name(), tflags[j], pflags[j])
}
}
// Check the next level
checkCommand(t, pp.Commands(), tt.next)
}
}
func TestPluginDynamicCompletion(t *testing.T) {
tests := []cmdTestCase{{
name: "completion for plugin",
cmd: "__complete args ''",
golden: "output/plugin_args_comp.txt",
rels: []*release.Release{},
}, {
name: "completion for plugin with flag",
cmd: "__complete args --myflag ''",
golden: "output/plugin_args_flag_comp.txt",
rels: []*release.Release{},
}, {
name: "completion for plugin with global flag",
cmd: "__complete args --namespace mynamespace ''",
golden: "output/plugin_args_ns_comp.txt",
rels: []*release.Release{},
}, {
name: "completion for plugin with multiple args",
cmd: "__complete args --myflag --namespace mynamespace start",
golden: "output/plugin_args_many_args_comp.txt",
rels: []*release.Release{},
}, {
name: "completion for plugin no directive",
cmd: "__complete echo -n mynamespace ''",
golden: "output/plugin_echo_no_directive.txt",
rels: []*release.Release{},
}, {
name: "completion for plugin bad directive",
cmd: "__complete echo ''",
golden: "output/plugin_echo_bad_directive.txt",
rels: []*release.Release{},
}}
for _, test := range tests {
settings.PluginsDirectory = "testdata/helmhome/helm/plugins"
runTestCmd(t, []cmdTestCase{test})
}
}
func TestLoadPlugins_HelmNoPlugins(t *testing.T) {
settings.PluginsDirectory = "testdata/helmhome/helm/plugins"
settings.RepositoryConfig = "testdata/helmhome/helm/repository"

@ -20,7 +20,6 @@ import (
"fmt"
"os"
"path/filepath"
"regexp"
"testing"
"helm.sh/helm/v3/pkg/repo/repotest"
@ -37,6 +36,10 @@ func TestPullCmd(t *testing.T) {
t.Fatal(err)
}
helmTestKeyOut := "Signed by: Helm Testing (This key should only be used for testing. DO NOT TRUST.) <helm-testing@helm.sh>\n" +
"Using Key With Fingerprint: 5E615389B53CA37F0EE60BD3843BBF981FC18762\n" +
"Chart Hash Verified: "
// all flags will get "-d outdir" appended.
tests := []struct {
name string
@ -49,6 +52,7 @@ func TestPullCmd(t *testing.T) {
expectFile string
expectDir bool
expectVerify bool
expectSha string
}{
{
name: "Basic chart fetch",
@ -77,6 +81,7 @@ func TestPullCmd(t *testing.T) {
args: "test/signtest --verify --keyring testdata/helm-test-key.pub",
expectFile: "./signtest-0.1.0.tgz",
expectVerify: true,
expectSha: "sha256:e5ef611620fb97704d8751c16bab17fedb68883bfb0edc76f78a70e9173f9b55",
},
{
name: "Fetch and fail verify",
@ -110,6 +115,7 @@ func TestPullCmd(t *testing.T) {
expectFile: "./signtest2",
expectDir: true,
expectVerify: true,
expectSha: "sha256:e5ef611620fb97704d8751c16bab17fedb68883bfb0edc76f78a70e9173f9b55",
},
{
name: "Chart fetch using repo URL",
@ -171,13 +177,11 @@ func TestPullCmd(t *testing.T) {
}
if tt.expectVerify {
pointerAddressPattern := "0[xX][A-Fa-f0-9]+"
sha256Pattern := "[A-Fa-f0-9]{64}"
verificationRegex := regexp.MustCompile(
fmt.Sprintf("Verification: &{%s sha256:%s signtest-0.1.0.tgz}\n", pointerAddressPattern, sha256Pattern))
if !verificationRegex.MatchString(out) {
t.Errorf("%q: expected match for regex %s, got %s", tt.name, verificationRegex, out)
outString := helmTestKeyOut + tt.expectSha + "\n"
if out != outString {
t.Errorf("%q: expected verification output %q, got %q", tt.name, outString, out)
}
}
ef := filepath.Join(outdir, tt.expectFile)

@ -46,6 +46,7 @@ type repoAddOptions struct {
certFile string
keyFile string
caFile string
insecureSkipTLSverify bool
repoFile string
repoCache string
@ -75,6 +76,7 @@ func newRepoAddCmd(out io.Writer) *cobra.Command {
f.StringVar(&o.certFile, "cert-file", "", "identify HTTPS client using this SSL certificate file")
f.StringVar(&o.keyFile, "key-file", "", "identify HTTPS client using this SSL key file")
f.StringVar(&o.caFile, "ca-file", "", "verify certificates of HTTPS-enabled servers using this CA bundle")
f.BoolVar(&o.insecureSkipTLSverify, "insecure-skip-tls-verify", false, "skip tls certificate checks for the repository")
return cmd
}
@ -120,6 +122,7 @@ func (o *repoAddOptions) run(out io.Writer) error {
CertFile: o.certFile,
KeyFile: o.keyFile,
CAFile: o.caFile,
InsecureSkipTLSverify: o.insecureSkipTLSverify,
}
r, err := repo.NewChartRepository(&c, getter.All(settings))

@ -24,37 +24,13 @@ import (
"github.com/spf13/cobra"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/tools/clientcmd"
"helm.sh/helm/v3/cmd/helm/require"
"helm.sh/helm/v3/internal/completion"
"helm.sh/helm/v3/internal/experimental/registry"
"helm.sh/helm/v3/pkg/action"
)
const (
contextCompFunc = `
__helm_get_contexts()
{
__helm_debug "${FUNCNAME[0]}: c is $c words[c] is ${words[c]}"
local template out
template="{{ range .contexts }}{{ .name }} {{ end }}"
if out=$(kubectl config -o template --template="${template}" view 2>/dev/null); then
COMPREPLY=( $( compgen -W "${out[*]}" -- "$cur" ) )
fi
}
`
)
var (
// Mapping of global flags that can have dynamic completion and the
// completion function to be used.
bashCompletionFlags = map[string]string{
// Cannot convert the kube-context flag to Go completion yet because
// an incomplete kube-context will make actionConfig.Init() fail at the very start
"kube-context": "__helm_get_contexts",
}
)
var globalUsage = `The Kubernetes package manager
Common actions for Helm:
@ -100,15 +76,14 @@ func newRootCmd(actionConfig *action.Configuration, out io.Writer, args []string
Short: "The Helm package manager for Kubernetes.",
Long: globalUsage,
SilenceUsage: true,
Args: require.NoArgs,
BashCompletionFunction: fmt.Sprintf("%s%s", contextCompFunc, completion.GetBashCustomFunction()),
BashCompletionFunction: completion.GetBashCustomFunction(),
}
flags := cmd.PersistentFlags()
settings.AddFlags(flags)
flag := flags.Lookup("namespace")
// Setup shell completion for the namespace flag
flag := flags.Lookup("namespace")
completion.RegisterFlagCompletionFunc(flag, func(cmd *cobra.Command, args []string, toComplete string) ([]string, completion.BashCompDirective) {
if client, err := actionConfig.KubernetesClientSet(); err == nil {
// Choose a long enough timeout that the user notices somethings is not working
@ -129,6 +104,29 @@ func newRootCmd(actionConfig *action.Configuration, out io.Writer, args []string
return nil, completion.BashCompDirectiveDefault
})
// Setup shell completion for the kube-context flag
flag = flags.Lookup("kube-context")
completion.RegisterFlagCompletionFunc(flag, func(cmd *cobra.Command, args []string, toComplete string) ([]string, completion.BashCompDirective) {
completion.CompDebugln("About to get the different kube-contexts")
loadingRules := clientcmd.NewDefaultClientConfigLoadingRules()
if len(settings.KubeConfig) > 0 {
loadingRules = &clientcmd.ClientConfigLoadingRules{ExplicitPath: settings.KubeConfig}
}
if config, err := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(
loadingRules,
&clientcmd.ConfigOverrides{}).RawConfig(); err == nil {
ctxs := []string{}
for name := range config.Contexts {
if strings.HasPrefix(name, toComplete) {
ctxs = append(ctxs, name)
}
}
return ctxs, completion.BashCompDirectiveNoFileComp
}
return nil, completion.BashCompDirectiveNoFileComp
})
// We can safely ignore any errors that flags.Parse encounters since
// those errors will be caught later during the call to cmd.Execution.
// This call is required to gather configuration information prior to
@ -173,19 +171,6 @@ func newRootCmd(actionConfig *action.Configuration, out io.Writer, args []string
completion.NewCompleteCmd(settings, out),
)
// Add annotation to flags for which we can generate completion choices
for name, completion := range bashCompletionFlags {
if cmd.Flag(name) != nil {
if cmd.Flag(name).Annotations == nil {
cmd.Flag(name).Annotations = map[string][]string{}
}
cmd.Flag(name).Annotations[cobra.BashCompCustom] = append(
cmd.Flag(name).Annotations[cobra.BashCompCustom],
completion,
)
}
}
// Add *experimental* subcommands
registryClient, err := registry.NewClient(
registry.ClientOptDebug(settings.Debug),

@ -95,3 +95,11 @@ func TestRootCmd(t *testing.T) {
})
}
}
func TestUnknownSubCmd(t *testing.T) {
_, _, err := executeActionCommand("foobar")
if err == nil || err.Error() != `unknown command "foobar" for "helm"` {
t.Errorf("Expect unknown command error, got %q", err)
}
}

@ -22,6 +22,7 @@ import (
"fmt"
"io"
"io/ioutil"
"os"
"path/filepath"
"strings"
@ -102,7 +103,7 @@ func newSearchRepoCmd(out io.Writer) *cobra.Command {
func (o *searchRepoOptions) run(out io.Writer, args []string) error {
o.setupSearchedVersion()
index, err := o.buildIndex(out)
index, err := o.buildIndex()
if err != nil {
return err
}
@ -171,7 +172,7 @@ func (o *searchRepoOptions) applyConstraint(res []*search.Result) ([]*search.Res
return data, nil
}
func (o *searchRepoOptions) buildIndex(out io.Writer) (*search.Index, error) {
func (o *searchRepoOptions) buildIndex() (*search.Index, error) {
// Load the repositories.yaml
rf, err := repo.LoadFile(o.repoFile)
if isNotExist(err) || len(rf.Repositories) == 0 {
@ -184,8 +185,7 @@ func (o *searchRepoOptions) buildIndex(out io.Writer) (*search.Index, error) {
f := filepath.Join(o.repoCacheDir, helmpath.CacheIndexFile(n))
ind, err := repo.LoadIndexFile(f)
if err != nil {
// TODO should print to stderr
fmt.Fprintf(out, "WARNING: Repo %q is corrupt or missing. Try 'helm repo update'.", n)
fmt.Fprintf(os.Stderr, "WARNING: Repo %q is corrupt or missing. Try 'helm repo update'.", n)
continue
}

@ -77,11 +77,7 @@ func newShowCmd(out io.Writer) *cobra.Command {
Args: require.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
client.OutputFormat = action.ShowAll
cp, err := client.ChartPathOptions.LocateChart(args[0], settings)
if err != nil {
return err
}
output, err := client.Run(cp)
output, err := runShow(args, client)
if err != nil {
return err
}
@ -97,11 +93,7 @@ func newShowCmd(out io.Writer) *cobra.Command {
Args: require.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
client.OutputFormat = action.ShowValues
cp, err := client.ChartPathOptions.LocateChart(args[0], settings)
if err != nil {
return err
}
output, err := client.Run(cp)
output, err := runShow(args, client)
if err != nil {
return err
}
@ -117,11 +109,7 @@ func newShowCmd(out io.Writer) *cobra.Command {
Args: require.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
client.OutputFormat = action.ShowChart
cp, err := client.ChartPathOptions.LocateChart(args[0], settings)
if err != nil {
return err
}
output, err := client.Run(cp)
output, err := runShow(args, client)
if err != nil {
return err
}
@ -137,11 +125,7 @@ func newShowCmd(out io.Writer) *cobra.Command {
Args: require.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
client.OutputFormat = action.ShowReadme
cp, err := client.ChartPathOptions.LocateChart(args[0], settings)
if err != nil {
return err
}
output, err := client.Run(cp)
output, err := runShow(args, client)
if err != nil {
return err
}
@ -152,8 +136,7 @@ func newShowCmd(out io.Writer) *cobra.Command {
cmds := []*cobra.Command{all, readmeSubCmd, valuesSubCmd, chartSubCmd}
for _, subCmd := range cmds {
addChartPathOptionsFlags(subCmd.Flags(), &client.ChartPathOptions)
showCommand.AddCommand(subCmd)
addShowFlags(showCommand, subCmd, client)
// Register the completion function for each subcommand
completion.RegisterValidArgsFunc(subCmd, validArgsFunc)
@ -161,3 +144,25 @@ func newShowCmd(out io.Writer) *cobra.Command {
return showCommand
}
func addShowFlags(showCmd *cobra.Command, subCmd *cobra.Command, client *action.Show) {
f := subCmd.Flags()
f.BoolVar(&client.Devel, "devel", false, "use development versions, too. Equivalent to version '>0.0.0-0'. If --version is set, this is ignored")
addChartPathOptionsFlags(f, &client.ChartPathOptions)
showCmd.AddCommand(subCmd)
}
func runShow(args []string, client *action.Show) (string, error) {
debug("Original chart version: %q", client.Version)
if client.Version == "" && client.Devel {
debug("setting version to >0.0.0-0")
client.Version = ">0.0.0-0"
}
cp, err := client.ChartPathOptions.LocateChart(args[0], settings)
if err != nil {
return "", err
}
return client.Run(cp)
}

@ -0,0 +1,82 @@
/*
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 main
import (
"fmt"
"path/filepath"
"strings"
"testing"
"helm.sh/helm/v3/pkg/repo/repotest"
)
func TestShowPreReleaseChart(t *testing.T) {
srv, err := repotest.NewTempServer("testdata/testcharts/*.tgz*")
if err != nil {
t.Fatal(err)
}
defer srv.Stop()
if err := srv.LinkIndices(); err != nil {
t.Fatal(err)
}
tests := []struct {
name string
args string
flags string
fail bool
expectedErr string
}{
{
name: "show pre-release chart",
args: "test/pre-release-chart",
fail: true,
expectedErr: "failed to download \"test/pre-release-chart\"",
},
{
name: "show pre-release chart with 'devel' flag",
args: "test/pre-release-chart",
flags: "--devel",
fail: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
outdir := srv.Root()
cmd := fmt.Sprintf("show all '%s' %s --repository-config %s --repository-cache %s",
tt.args,
tt.flags,
filepath.Join(outdir, "repositories.yaml"),
outdir,
)
//_, out, err := executeActionCommand(cmd)
_, _, err := executeActionCommand(cmd)
if err != nil {
if tt.fail {
if !strings.Contains(err.Error(), tt.expectedErr) {
t.Errorf("%q expected error: %s, got: %s", tt.name, tt.expectedErr, err.Error())
}
return
}
t.Errorf("%q reported error: %s", tt.name, err)
}
})
}
}

@ -24,8 +24,6 @@ import (
"regexp"
"strings"
"helm.sh/helm/v3/pkg/releaseutil"
"github.com/spf13/cobra"
"helm.sh/helm/v3/cmd/helm/require"
@ -33,6 +31,7 @@ import (
"helm.sh/helm/v3/pkg/action"
"helm.sh/helm/v3/pkg/chartutil"
"helm.sh/helm/v3/pkg/cli/values"
"helm.sh/helm/v3/pkg/releaseutil"
)
const templateDesc = `
@ -53,7 +52,7 @@ func newTemplateCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
cmd := &cobra.Command{
Use: "template [NAME] [CHART]",
Short: fmt.Sprintf("locally render templates"),
Short: "locally render templates",
Long: templateDesc,
Args: require.MinimumNArgs(1),
RunE: func(_ *cobra.Command, args []string) error {
@ -64,10 +63,17 @@ func newTemplateCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
client.APIVersions = chartutil.VersionSet(extraAPIs)
client.IncludeCRDs = includeCrds
rel, err := runInstall(args, client, valueOpts, out)
if err != nil {
if err != nil && !settings.Debug {
if rel != nil {
return fmt.Errorf("%w\n\nUse --debug flag to render out invalid YAML", err)
}
return err
}
// We ignore a potential error here because, when the --debug flag was specified,
// we always want to print the YAML, even if it is not valid. The error is still returned afterwards.
if rel != nil {
var manifests bytes.Buffer
fmt.Fprintln(&manifests, strings.TrimSpace(rel.Manifest))
@ -113,8 +119,9 @@ func newTemplateCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
} else {
fmt.Fprintf(out, "%s", manifests.String())
}
}
return nil
return err
},
}

@ -102,6 +102,18 @@ func TestTemplateCmd(t *testing.T) {
// don't accidentally get the expected result.
repeat: 10,
},
{
name: "chart with template with invalid yaml",
cmd: fmt.Sprintf("template '%s'", "testdata/testcharts/chart-with-template-with-invalid-yaml"),
wantError: true,
golden: "output/template-with-invalid-yaml.txt",
},
{
name: "chart with template with invalid yaml (--debug)",
cmd: fmt.Sprintf("template '%s' --debug", "testdata/testcharts/chart-with-template-with-invalid-yaml"),
wantError: true,
golden: "output/template-with-invalid-yaml-debug.txt",
},
}
runTestCmd(t, tests)
}

@ -0,0 +1,13 @@
#!/usr/bin/env sh
echo "plugin.complete was called"
echo "Namespace: ${HELM_NAMESPACE:-NO_NS}"
echo "Num args received: ${#}"
echo "Args received: ${@}"
# Final printout is the optional completion directive of the form :<directive>
if [ "$HELM_NAMESPACE" = "default" ]; then
echo ":4"
else
echo ":2"
fi

@ -0,0 +1,14 @@
#!/usr/bin/env sh
echo "echo plugin.complete was called"
echo "Namespace: ${HELM_NAMESPACE:-NO_NS}"
echo "Num args received: ${#}"
echo "Args received: ${@}"
# Final printout is the optional completion directive of the form :<directive>
if [ "$HELM_NAMESPACE" = "default" ]; then
# Output an invalid directive, which should be ignored
echo ":2222"
# else
# Don't include the directive, to test it is really optional
fi

@ -0,0 +1,13 @@
name: env
commands:
- name: list
flags:
- a
- all
- log
- name: remove
validArgs:
- all
- one
flags:
- global

@ -0,0 +1,5 @@
commands:
- name: code
flags:
- a
- b

@ -0,0 +1,19 @@
name: wrongname
commands:
- name: empty
- name: full
commands:
- name: more
validArgs:
- one
- two
flags:
- b
- ball
- name: less
flags:
- a
- all
flags:
- z
- q

@ -0,0 +1,5 @@
plugin.complete was called
Namespace: default
Num args received: 1
Args received:
:4

@ -0,0 +1,5 @@
plugin.complete was called
Namespace: default
Num args received: 2
Args received: --myflag
:4

@ -0,0 +1,5 @@
plugin.complete was called
Namespace: mynamespace
Num args received: 2
Args received: --myflag start
:2

@ -0,0 +1,5 @@
plugin.complete was called
Namespace: mynamespace
Num args received: 1
Args received:
:2

@ -0,0 +1,5 @@
echo plugin.complete was called
Namespace: default
Num args received: 1
Args received:
:0

@ -0,0 +1,5 @@
echo plugin.complete was called
Namespace: mynamespace
Num args received: 1
Args received:
:0

@ -0,0 +1,13 @@
---
# Source: chart-with-template-with-invalid-yaml/templates/alpine-pod.yaml
apiVersion: v1
kind: Pod
metadata:
name: "RELEASE-NAME-my-alpine"
spec:
containers:
- name: waiter
image: "alpine:3.9"
command: ["/bin/sleep","9000"]
invalid
Error: YAML parse error on chart-with-template-with-invalid-yaml/templates/alpine-pod.yaml: error converting YAML to JSON: yaml: line 11: could not find expected ':'

@ -0,0 +1,3 @@
Error: YAML parse error on chart-with-template-with-invalid-yaml/templates/alpine-pod.yaml: error converting YAML to JSON: yaml: line 11: could not find expected ':'
Use --debug flag to render out invalid YAML

@ -1,4 +1,4 @@
#Alpine: A simple Helm chart
# Alpine: A simple Helm chart
Run a single pod of Alpine Linux.

@ -0,0 +1,8 @@
apiVersion: v1
description: Deploy a basic Alpine Linux pod
home: https://helm.sh/helm
name: chart-with-template-with-invalid-yaml
sources:
- https://github.com/helm/helm
version: 0.1.0
type: application

@ -0,0 +1,13 @@
#Alpine: A simple Helm chart
Run a single pod of Alpine Linux.
This example was generated using the command `helm create alpine`.
The `templates/` directory contains a very simple pod resource with a
couple of parameters.
The `values.yaml` file contains the default values for the
`alpine-pod.yaml` template.
You can install this example using `helm install ./alpine`.

@ -0,0 +1,10 @@
apiVersion: v1
kind: Pod
metadata:
name: "{{.Release.Name}}-{{.Values.Name}}"
spec:
containers:
- name: waiter
image: "alpine:3.9"
command: ["/bin/sleep","9000"]
invalid

@ -65,6 +65,7 @@ func newUpgradeCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
client := action.NewUpgrade(cfg)
valueOpts := &values.Options{}
var outfmt output.Format
var createNamespace bool
cmd := &cobra.Command{
Use: "upgrade [RELEASE] [CHART]",
@ -90,8 +91,7 @@ func newUpgradeCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
}
if client.Install {
// If a release does not exist, install it. If another error occurs during
// the check, ignore the error and continue with the upgrade.
// If a release does not exist, install it.
histClient := action.NewHistory(cfg)
histClient.Max = 1
if _, err := histClient.Run(args[0]); err == driver.ErrReleaseNotFound {
@ -100,21 +100,26 @@ func newUpgradeCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
fmt.Fprintf(out, "Release %q does not exist. Installing it now.\n", args[0])
}
instClient := action.NewInstall(cfg)
instClient.CreateNamespace = createNamespace
instClient.ChartPathOptions = client.ChartPathOptions
instClient.DryRun = client.DryRun
instClient.DisableHooks = client.DisableHooks
instClient.SkipCRDs = client.SkipCRDs
instClient.Timeout = client.Timeout
instClient.Wait = client.Wait
instClient.Devel = client.Devel
instClient.Namespace = client.Namespace
instClient.Atomic = client.Atomic
instClient.PostRenderer = client.PostRenderer
instClient.DisableOpenAPIValidation = client.DisableOpenAPIValidation
rel, err := runInstall(args, instClient, valueOpts, out)
if err != nil {
return err
}
return outfmt.Write(out, &statusPrinter{rel, settings.Debug})
} else if err != nil {
return err
}
}
@ -158,6 +163,7 @@ func newUpgradeCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
})
f := cmd.Flags()
f.BoolVar(&createNamespace, "create-namespace", false, "if --install is set, create the release namespace if not present")
f.BoolVarP(&client.Install, "install", "i", false, "if a release by this name doesn't already exist, run an install")
f.BoolVar(&client.Devel, "devel", false, "use development versions, too. Equivalent to version '>0.0.0-0'. If --version is set, this is ignored")
f.BoolVar(&client.DryRun, "dry-run", false, "simulate an upgrade")
@ -165,6 +171,8 @@ func newUpgradeCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
f.MarkDeprecated("recreate-pods", "functionality will no longer be updated. Consult the documentation for other methods to recreate pods")
f.BoolVar(&client.Force, "force", false, "force resource updates through a replacement strategy")
f.BoolVar(&client.DisableHooks, "no-hooks", false, "disable pre/post upgrade hooks")
f.BoolVar(&client.DisableOpenAPIValidation, "disable-openapi-validation", false, "if set, the upgrade process will not validate rendered templates against the Kubernetes OpenAPI Schema")
f.BoolVar(&client.SkipCRDs, "skip-crds", false, "if set, no CRDs will be installed when an upgrade is performed with install flag enabled. By default, CRDs are installed if not already present, when an upgrade is performed with install flag enabled")
f.DurationVar(&client.Timeout, "timeout", 300*time.Second, "time to wait for any individual Kubernetes operation (like Jobs for hooks)")
f.BoolVar(&client.ResetValues, "reset-values", false, "when upgrading, reset the values to the ones built into the chart")
f.BoolVar(&client.ReuseValues, "reuse-values", false, "when upgrading, reuse the last release's values and merge in any overrides from the command line via --set and -f. If '--reset-values' is specified, this is ignored")

@ -16,6 +16,7 @@ limitations under the License.
package main
import (
"fmt"
"io"
"github.com/spf13/cobra"
@ -44,7 +45,14 @@ func newVerifyCmd(out io.Writer) *cobra.Command {
Long: verifyDesc,
Args: require.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
return client.Run(args[0])
err := client.Run(args[0])
if err != nil {
return err
}
fmt.Fprint(out, client.Out)
return nil
},
}

@ -65,7 +65,7 @@ func TestVerifyCmd(t *testing.T) {
{
name: "verify validates a properly signed chart",
cmd: "verify testdata/testcharts/signtest-0.1.0.tgz --keyring testdata/helm-test-key.pub",
expect: "",
expect: "Signed by: Helm Testing (This key should only be used for testing. DO NOT TRUST.) <helm-testing@helm.sh>\nUsing Key With Fingerprint: 5E615389B53CA37F0EE60BD3843BBF981FC18762\nChart Hash Verified: sha256:e5ef611620fb97704d8751c16bab17fedb68883bfb0edc76f78a70e9173f9b55\n",
wantError: false,
},
}

@ -18,7 +18,7 @@ require (
github.com/gobwas/glob v0.2.3
github.com/gofrs/flock v0.7.1
github.com/gosuri/uitable v0.0.4
github.com/mattn/go-shellwords v1.0.9
github.com/mattn/go-shellwords v1.0.10
github.com/mitchellh/copystructure v1.0.0
github.com/opencontainers/go-digest v1.0.0-rc1
github.com/opencontainers/image-spec v1.0.1
@ -29,13 +29,13 @@ require (
github.com/stretchr/testify v1.4.0
github.com/xeipuuv/gojsonschema v1.1.0
golang.org/x/crypto v0.0.0-20200128174031-69ecbb4d6d5d
k8s.io/api v0.17.2
k8s.io/apiextensions-apiserver v0.17.2
k8s.io/apimachinery v0.17.2
k8s.io/cli-runtime v0.17.2
k8s.io/client-go v0.17.2
k8s.io/api v0.17.3
k8s.io/apiextensions-apiserver v0.17.3
k8s.io/apimachinery v0.17.3
k8s.io/cli-runtime v0.17.3
k8s.io/client-go v0.17.3
k8s.io/klog v1.0.0
k8s.io/kubectl v0.17.2
k8s.io/kubectl v0.17.3
sigs.k8s.io/yaml v1.1.0
)

@ -332,8 +332,8 @@ github.com/mattn/go-isatty v0.0.4 h1:bnP0vzxcAdeI1zdubAl5PjU6zsERjGZb7raWodagDYs
github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-runewidth v0.0.2 h1:UnlwIPBGaTZfPQ6T1IGzPI0EkYAQmT9fAEJ/poFC63o=
github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/mattn/go-shellwords v1.0.9 h1:eaB5JspOwiKKcHdqcjbfe5lA9cNn/4NRRtddXJCimqk=
github.com/mattn/go-shellwords v1.0.9/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y=
github.com/mattn/go-shellwords v1.0.10 h1:Y7Xqm8piKOO3v10Thp7Z36h4FYFjt5xB//6XvOrs2Gw=
github.com/mattn/go-shellwords v1.0.10/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y=
github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/mitchellh/copystructure v1.0.0 h1:Laisrj+bAB6b/yJwB5Bt3ITZhGJdqmxquMKeZ+mmkFQ=
@ -630,25 +630,27 @@ gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo=
gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
k8s.io/api v0.17.2 h1:NF1UFXcKN7/OOv1uxdRz3qfra8AHsPav5M93hlV9+Dc=
k8s.io/api v0.17.2/go.mod h1:BS9fjjLc4CMuqfSO8vgbHPKMt5+SF0ET6u/RVDihTo4=
k8s.io/apiextensions-apiserver v0.17.2 h1:cP579D2hSZNuO/rZj9XFRzwJNYb41DbNANJb6Kolpss=
k8s.io/apiextensions-apiserver v0.17.2/go.mod h1:4KdMpjkEjjDI2pPfBA15OscyNldHWdBCfsWMDWAmSTs=
k8s.io/apimachinery v0.17.2 h1:hwDQQFbdRlpnnsR64Asdi55GyCaIP/3WQpMmbNBeWr4=
k8s.io/apimachinery v0.17.2/go.mod h1:b9qmWdKlLuU9EBh+06BtLcSf/Mu89rWL33naRxs1uZg=
k8s.io/apiserver v0.17.2/go.mod h1:lBmw/TtQdtxvrTk0e2cgtOxHizXI+d0mmGQURIHQZlo=
k8s.io/cli-runtime v0.17.2 h1:YH4txSplyGudvxjhAJeHEtXc7Tr/16clKGfN076ydGk=
k8s.io/cli-runtime v0.17.2/go.mod h1:aa8t9ziyQdbkuizkNLAw3qe3srSyWh9zlSB7zTqRNPI=
k8s.io/client-go v0.17.2 h1:ndIfkfXEGrNhLIgkr0+qhRguSD3u6DCmonepn1O6NYc=
k8s.io/client-go v0.17.2/go.mod h1:QAzRgsa0C2xl4/eVpeVAZMvikCn8Nm81yqVx3Kk9XYI=
k8s.io/code-generator v0.17.2/go.mod h1:DVmfPQgxQENqDIzVR2ddLXMH34qeszkKSdH/N+s+38s=
k8s.io/component-base v0.17.2 h1:0XHf+cerTvL9I5Xwn9v+0jmqzGAZI7zNydv4tL6Cw6A=
k8s.io/component-base v0.17.2/go.mod h1:zMPW3g5aH7cHJpKYQ/ZsGMcgbsA/VyhEugF3QT1awLs=
k8s.io/api v0.17.3 h1:XAm3PZp3wnEdzekNkcmj/9Y1zdmQYJ1I4GKSBBZ8aG0=
k8s.io/api v0.17.3/go.mod h1:YZ0OTkuw7ipbe305fMpIdf3GLXZKRigjtZaV5gzC2J0=
k8s.io/apiextensions-apiserver v0.17.3 h1:WDZWkPcbgvchEdDd7ysL21GGPx3UKZQLDZXEkevT6n4=
k8s.io/apiextensions-apiserver v0.17.3/go.mod h1:CJbCyMfkKftAd/X/V6OTHYhVn7zXnDdnkUjS1h0GTeY=
k8s.io/apimachinery v0.17.3 h1:f+uZV6rm4/tHE7xXgLyToprg6xWairaClGVkm2t8omg=
k8s.io/apimachinery v0.17.3/go.mod h1:gxLnyZcGNdZTCLnq3fgzyg2A5BVCHTNDFrw8AmuJ+0g=
k8s.io/apiserver v0.17.3/go.mod h1:iJtsPpu1ZpEnHaNawpSV0nYTGBhhX2dUlnn7/QS7QiY=
k8s.io/cli-runtime v0.17.3 h1:0ZlDdJgJBKsu77trRUynNiWsRuAvAVPBNaQfnt/1qtc=
k8s.io/cli-runtime v0.17.3/go.mod h1:X7idckYphH4SZflgNpOOViSxetiMj6xI0viMAjM81TA=
k8s.io/client-go v0.17.3 h1:deUna1Ksx05XeESH6XGCyONNFfiQmDdqeqUvicvP6nU=
k8s.io/client-go v0.17.3/go.mod h1:cLXlTMtWHkuK4tD360KpWz2gG2KtdWEr/OT02i3emRQ=
k8s.io/code-generator v0.17.3/go.mod h1:l8BLVwASXQZTo2xamW5mQNFCe1XPiAesVq7Y1t7PiQQ=
k8s.io/component-base v0.17.3 h1:hQzTSshY14aLSR6WGIYvmw+w+u6V4d+iDR2iDGMrlUg=
k8s.io/component-base v0.17.3/go.mod h1:GeQf4BrgelWm64PXkIXiPh/XS0hnO42d9gx9BtbZRp8=
k8s.io/gengo v0.0.0-20190128074634-0689ccc1d7d6/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0=
k8s.io/gengo v0.0.0-20190822140433-26a664648505/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0=
k8s.io/klog v0.0.0-20181102134211-b9b56d5dfc92/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk=
@ -657,10 +659,10 @@ k8s.io/klog v1.0.0 h1:Pt+yjF5aB1xDSVbau4VsWe+dQNzA0qv1LlXdC2dF6Q8=
k8s.io/klog v1.0.0/go.mod h1:4Bi6QPql/J/LkTDqv7R/cd3hPo4k2DG6Ptcz060Ez5I=
k8s.io/kube-openapi v0.0.0-20191107075043-30be4d16710a h1:UcxjrRMyNx/i/y8G7kPvLyy7rfbeuf1PYyBf973pgyU=
k8s.io/kube-openapi v0.0.0-20191107075043-30be4d16710a/go.mod h1:1TqjTSzOxsLGIKfj0lK8EeCP7K1iUG65v09OM0/WG5E=
k8s.io/kubectl v0.17.2 h1:QZR8Q6lWiVRjwKslekdbN5WPMp53dS/17j5e+oi5XVU=
k8s.io/kubectl v0.17.2/go.mod h1:y4rfLV0n6aPmvbRCqZQjvOp3ezxsFgpqL+zF5jH/lxk=
k8s.io/kubectl v0.17.3 h1:9HHYj07kuFkM+sMJMOyQX29CKWq4lvKAG1UIPxNPMQ4=
k8s.io/kubectl v0.17.3/go.mod h1:NUn4IBY7f7yCMwSop2HCXlw/MVYP4HJBiUmOR3n9w28=
k8s.io/kubernetes v1.13.0/go.mod h1:ocZa8+6APFNC2tX1DZASIbocyYT5jHzqFVsY5aoB7Jk=
k8s.io/metrics v0.17.2/go.mod h1:3TkNHET4ROd+NfzNxkjoVfQ0Ob4iZnaHmSEA4vYpwLw=
k8s.io/metrics v0.17.3/go.mod h1:HEJGy1fhHOjHggW9rMDBJBD3YuGroH3Y1pnIRw9FFaI=
k8s.io/utils v0.0.0-20191114184206-e782cd3c129f h1:GiPwtSzdP43eI1hpPCbROQCCIgCuiMMNF8YUVLF3vJo=
k8s.io/utils v0.0.0-20191114184206-e782cd3c129f/go.mod h1:sZAwmy6armz5eXlNoLmJcl4F1QuKu7sr+mFQ0byX7Ew=
modernc.org/cc v1.0.0/go.mod h1:1Sk4//wdnYJiUIxnW8ddKpaOJCF37yAdqYnkxUpaYxw=

@ -35,9 +35,9 @@ import (
// This should ultimately be pushed down into Cobra.
// ==================================================================================
// compRequestCmd Hidden command to request completion results from the program.
// CompRequestCmd Hidden command to request completion results from the program.
// Used by the shell completion script.
const compRequestCmd = "__complete"
const CompRequestCmd = "__complete"
// Global map allowing to find completion functions for commands or flags.
var validArgsFunctions = map[interface{}]func(cmd *cobra.Command, args []string, toComplete string) ([]string, BashCompDirective){}
@ -123,7 +123,7 @@ __helm_custom_func()
done < <(compgen -W "${out[*]}" -- "$cur")
fi
}
`, compRequestCmd, BashCompDirectiveError, BashCompDirectiveNoSpace, BashCompDirectiveNoFileComp)
`, CompRequestCmd, BashCompDirectiveError, BashCompDirectiveNoSpace, BashCompDirectiveNoFileComp)
}
// RegisterValidArgsFunc should be called to register a function to provide argument completion for a command
@ -177,40 +177,58 @@ func (d BashCompDirective) string() string {
func NewCompleteCmd(settings *cli.EnvSettings, out io.Writer) *cobra.Command {
debug = settings.Debug
return &cobra.Command{
Use: fmt.Sprintf("%s [command-line]", compRequestCmd),
Use: fmt.Sprintf("%s [command-line]", CompRequestCmd),
DisableFlagsInUseLine: true,
Hidden: true,
DisableFlagParsing: true,
Args: require.MinimumNArgs(1),
Short: "Request shell completion choices for the specified command-line",
Long: fmt.Sprintf("%s is a special command that is used by the shell completion logic\n%s",
compRequestCmd, "to request completion choices for the specified command-line."),
CompRequestCmd, "to request completion choices for the specified command-line."),
Run: func(cmd *cobra.Command, args []string) {
CompDebugln(fmt.Sprintf("%s was called with args %v", cmd.Name(), args))
flag, trimmedArgs, toComplete, err := checkIfFlagCompletion(cmd.Root(), args[:len(args)-1], args[len(args)-1])
if err != nil {
// Error while attempting to parse flags
CompErrorln(err.Error())
return
}
// The last argument, which is not complete, should not be part of the list of arguments
toComplete := args[len(args)-1]
trimmedArgs := args[:len(args)-1]
// Find the real command for which completion must be performed
finalCmd, finalArgs, err := cmd.Root().Find(trimmedArgs)
if err != nil {
// Unable to find the real command. E.g., helm invalidCmd <TAB>
CompDebugln(fmt.Sprintf("Unable to find a command for arguments: %v", trimmedArgs))
return
}
CompDebugln(fmt.Sprintf("Found final command '%s', with finalArgs %v", finalCmd.Name(), finalArgs))
var flag *pflag.Flag
if !finalCmd.DisableFlagParsing {
// We only do flag completion if we are allowed to parse flags
// This is important for helm plugins which need to do their own flag completion.
flag, finalArgs, toComplete, err = checkIfFlagCompletion(finalCmd, finalArgs, toComplete)
if err != nil {
// Error while attempting to parse flags
CompErrorln(err.Error())
return
}
}
// Parse the flags and extract the arguments to prepare for calling the completion function
if err = finalCmd.ParseFlags(finalArgs); err != nil {
CompErrorln(fmt.Sprintf("Error while parsing flags from args %v: %s", finalArgs, err.Error()))
return
}
argsWoFlags := finalCmd.Flags().Args()
CompDebugln(fmt.Sprintf("Args without flags are '%v' with length %d", argsWoFlags, len(argsWoFlags)))
// We only remove the flags from the arguments if DisableFlagParsing is not set.
// This is important for helm plugins, which need to receive all flags.
// The plugin completion code will do its own flag parsing.
if !finalCmd.DisableFlagParsing {
finalArgs = finalCmd.Flags().Args()
CompDebugln(fmt.Sprintf("Args without flags are '%v' with length %d", finalArgs, len(finalArgs)))
}
// Find completion function for the flag or command
var key interface{}
var keyStr string
if flag != nil {
@ -220,26 +238,28 @@ func NewCompleteCmd(settings *cli.EnvSettings, out io.Writer) *cobra.Command {
key = finalCmd
keyStr = finalCmd.Name()
}
// Find completion function for the flag or command
completionFn, ok := validArgsFunctions[key]
if !ok {
CompErrorln(fmt.Sprintf("Dynamic completion not supported/needed for flag or command: %s", keyStr))
return
}
CompDebugln(fmt.Sprintf("Calling completion method for subcommand '%s' with args '%v' and toComplete '%s'", finalCmd.Name(), argsWoFlags, toComplete))
completions, directive := completionFn(finalCmd, argsWoFlags, toComplete)
CompDebugln(fmt.Sprintf("Calling completion method for subcommand '%s' with args '%v' and toComplete '%s'", finalCmd.Name(), finalArgs, toComplete))
completions, directive := completionFn(finalCmd, finalArgs, toComplete)
for _, comp := range completions {
// Print each possible completion to stdout for the completion script to consume.
fmt.Fprintln(out, comp)
}
if directive > BashCompDirectiveError+BashCompDirectiveNoSpace+BashCompDirectiveNoFileComp {
directive = BashCompDirectiveDefault
}
// As the last printout, print the completion directive for the
// completion script to parse.
// The directive integer must be that last character following a single :
// The completion script expects :directive
fmt.Fprintln(out, fmt.Sprintf(":%d", directive))
fmt.Fprintf(out, ":%d\n", directive)
// Print some helpful info to stderr for the user to understand.
// Output from stderr should be ignored from the completion script.
@ -252,7 +272,7 @@ func isFlag(arg string) bool {
return len(arg) > 0 && arg[0] == '-'
}
func checkIfFlagCompletion(rootCmd *cobra.Command, args []string, lastArg string) (*pflag.Flag, []string, string, error) {
func checkIfFlagCompletion(finalCmd *cobra.Command, args []string, lastArg string) (*pflag.Flag, []string, string, error) {
var flagName string
trimmedArgs := args
flagWithEqual := false
@ -287,19 +307,10 @@ func checkIfFlagCompletion(rootCmd *cobra.Command, args []string, lastArg string
return nil, trimmedArgs, lastArg, nil
}
// Find the real command for which completion must be performed
finalCmd, _, err := rootCmd.Find(trimmedArgs)
if err != nil {
// Unable to find the real command. E.g., helm invalidCmd <TAB>
return nil, nil, "", errors.New("Unable to find final command for completion")
}
CompDebugln(fmt.Sprintf("checkIfFlagCompletion: found final command '%s'", finalCmd.Name()))
flag := findFlag(finalCmd, flagName)
if flag == nil {
// Flag not supported by this command, nothing to complete
err = fmt.Errorf("Subcommand '%s' does not support flag '%s'", finalCmd.Name(), flagName)
err := fmt.Errorf("Subcommand '%s' does not support flag '%s'", finalCmd.Name(), flagName)
return nil, nil, "", err
}
@ -357,7 +368,7 @@ func CompDebug(msg string) {
if debug {
// Must print to stderr for this not to be read by the completion script.
fmt.Fprintf(os.Stderr, msg)
fmt.Fprintln(os.Stderr, msg)
}
}
@ -378,7 +389,7 @@ func CompError(msg string) {
// If not already printed by the call to CompDebug().
if !debug {
// Must print to stderr for this not to be read by the completion script.
fmt.Fprintf(os.Stderr, msg)
fmt.Fprintln(os.Stderr, msg)
}
}

@ -162,13 +162,13 @@ func (suite *RegistryClientTestSuite) Test_2_LoadChart() {
// non-existent ref
ref, err := ParseReference(fmt.Sprintf("%s/testrepo/whodis:9.9.9", suite.DockerRegistryHost))
suite.Nil(err)
ch, err := suite.RegistryClient.LoadChart(ref)
_, err = suite.RegistryClient.LoadChart(ref)
suite.NotNil(err)
// existing ref
ref, err = ParseReference(fmt.Sprintf("%s/testrepo/testchart:1.2.3", suite.DockerRegistryHost))
suite.Nil(err)
ch, err = suite.RegistryClient.LoadChart(ref)
ch, err := suite.RegistryClient.LoadChart(ref)
suite.Nil(err)
suite.Equal("testchart", ch.Metadata.Name)
suite.Equal("1.2.3", ch.Metadata.Version)

@ -135,6 +135,7 @@ func (c *Configuration) getCapabilities() (*chartutil.Capabilities, error) {
return c.Capabilities, nil
}
// KubernetesClientSet creates a new kubernetes ClientSet based on the configuration
func (c *Configuration) KubernetesClientSet() (kubernetes.Interface, error) {
conf, err := c.RESTClientGetter.ToRESTConfig()
if err != nil {
@ -219,7 +220,7 @@ func (c *Configuration) recordRelease(r *release.Release) {
}
}
// InitActionConfig initializes the action configuration
// Init initializes the action configuration
func (c *Configuration) Init(getter genericclioptions.RESTClientGetter, namespace string, helmDriver string, log DebugLog) error {
kc := kube.New(getter)
kc.Log = log
@ -240,7 +241,18 @@ func (c *Configuration) Init(getter genericclioptions.RESTClientGetter, namespac
d.Log = log
store = storage.Init(d)
case "memory":
d := driver.NewMemory()
var d *driver.Memory
if c.Releases != nil {
if mem, ok := c.Releases.Driver.(*driver.Memory); ok {
// This function can be called more than once (e.g., helm list --all-namespaces).
// If a memory driver was already initialized, re-use it but set the possibly new namespace.
// We re-use it in case some releases where already created in the existing memory driver.
d = mem
}
}
if d == nil {
d = driver.NewMemory()
}
d.SetNamespace(namespace)
store = storage.Init(d)
default:

@ -230,6 +230,20 @@ func withSampleTemplates() chartOption {
}
}
func withSampleIncludingIncorrectTemplates() chartOption {
return func(opts *chartOptions) {
sampleTemplates := []*chart.File{
// This adds basic templates and partials.
{Name: "templates/goodbye", Data: []byte("goodbye: world")},
{Name: "templates/empty", Data: []byte("")},
{Name: "templates/incorrect", Data: []byte("{{ .Values.bad.doh }}")},
{Name: "templates/with-partials", Data: []byte(`hello: {{ template "_planet" . }}`)},
{Name: "templates/partials/_planet", Data: []byte(`{{define "_planet"}}Earth{{end}}`)},
}
opts.Templates = append(opts.Templates, sampleTemplates...)
}
}
func withMultipleManifestTemplate() chartOption {
return func(opts *chartOptions) {
sampleTemplates := []*chart.File{

@ -29,8 +29,11 @@ import (
"github.com/Masterminds/sprig/v3"
"github.com/pkg/errors"
v1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/cli-runtime/pkg/resource"
"sigs.k8s.io/yaml"
"helm.sh/helm/v3/pkg/chart"
"helm.sh/helm/v3/pkg/chartutil"
@ -38,6 +41,7 @@ import (
"helm.sh/helm/v3/pkg/downloader"
"helm.sh/helm/v3/pkg/engine"
"helm.sh/helm/v3/pkg/getter"
"helm.sh/helm/v3/pkg/kube"
kubefake "helm.sh/helm/v3/pkg/kube/fake"
"helm.sh/helm/v3/pkg/postrender"
"helm.sh/helm/v3/pkg/release"
@ -69,6 +73,7 @@ type Install struct {
ChartPathOptions
ClientOnly bool
CreateNamespace bool
DryRun bool
DisableHooks bool
Replace bool
@ -238,11 +243,18 @@ func (i *Install) Run(chrt *chart.Chart, vals map[string]interface{}) (*release.
// Mark this release as in-progress
rel.SetStatus(release.StatusPendingInstall, "Initial install underway")
var toBeAdopted kube.ResourceList
resources, err := i.cfg.KubeClient.Build(bytes.NewBufferString(rel.Manifest), !i.DisableOpenAPIValidation)
if err != nil {
return nil, errors.Wrap(err, "unable to build kubernetes objects from release manifest")
}
// It is safe to use "force" here because these are resources currently rendered by the chart.
err = resources.Visit(setMetadataVisitor(rel.Name, rel.Namespace, true))
if err != nil {
return nil, err
}
// Install requires an extra validation step of checking that resources
// don't already exist before we actually create resources. If we continue
// forward and create the release object with resources that already exist,
@ -250,7 +262,8 @@ func (i *Install) Run(chrt *chart.Chart, vals map[string]interface{}) (*release.
// deleting the release because the manifest will be pointing at that
// resource
if !i.ClientOnly && !isUpgrade {
if err := existingResourceConflict(resources); err != nil {
toBeAdopted, err = existingResourceConflict(resources, rel.Name, rel.Namespace)
if err != nil {
return nil, errors.Wrap(err, "rendered manifests contain a resource that already exists. Unable to continue with install")
}
}
@ -261,6 +274,32 @@ func (i *Install) Run(chrt *chart.Chart, vals map[string]interface{}) (*release.
return rel, nil
}
if i.CreateNamespace {
ns := &v1.Namespace{
TypeMeta: metav1.TypeMeta{
APIVersion: "v1",
Kind: "Namespace",
},
ObjectMeta: metav1.ObjectMeta{
Name: i.Namespace,
Labels: map[string]string{
"name": i.Namespace,
},
},
}
buf, err := yaml.Marshal(ns)
if err != nil {
return nil, err
}
resourceList, err := i.cfg.KubeClient.Build(bytes.NewBuffer(buf), true)
if err != nil {
return nil, err
}
if _, err := i.cfg.KubeClient.Create(resourceList); err != nil && !apierrors.IsAlreadyExists(err) {
return nil, err
}
}
// If Replace is true, we need to supercede the last release.
if i.Replace {
if err := i.replaceRelease(rel); err != nil {
@ -287,9 +326,15 @@ func (i *Install) Run(chrt *chart.Chart, vals map[string]interface{}) (*release.
// At this point, we can do the install. Note that before we were detecting whether to
// do an update, but it's not clear whether we WANT to do an update if the re-use is set
// to true, since that is basically an upgrade operation.
if len(toBeAdopted) == 0 {
if _, err := i.cfg.KubeClient.Create(resources); err != nil {
return i.failRelease(rel, err)
}
} else {
if _, err := i.cfg.KubeClient.Update(toBeAdopted, resources, false); err != nil {
return i.failRelease(rel, err)
}
}
if i.Wait {
if err := i.cfg.KubeClient.Wait(resources, i.Timeout); err != nil {
@ -710,6 +755,7 @@ func (c *ChartPathOptions) LocateChart(name string, settings *cli.EnvSettings) (
Getters: getter.All(settings),
Options: []getter.Option{
getter.WithBasicAuth(c.Username, c.Password),
getter.WithTLSClientConfig(c.CertFile, c.KeyFile, c.CaFile),
},
RepositoryConfig: settings.RepositoryConfig,
RepositoryCache: settings.RepositoryCache,

@ -240,6 +240,21 @@ func TestInstallRelease_DryRun(t *testing.T) {
is.Equal(res.Info.Description, "Dry run complete")
}
func TestInstallReleaseIncorrectTemplate_DryRun(t *testing.T) {
is := assert.New(t)
instAction := installAction(t)
instAction.DryRun = true
vals := map[string]interface{}{}
_, err := instAction.Run(buildChart(withSampleIncludingIncorrectTemplates()), vals)
expectedErr := "\"hello/templates/incorrect\" at <.Values.bad.doh>: nil pointer evaluating interface {}.doh"
if err == nil {
t.Fatalf("Install should fail containing error: %s", expectedErr)
}
if err != nil {
is.Contains(err.Error(), expectedErr)
}
}
func TestInstallRelease_NoHooks(t *testing.T) {
is := assert.New(t)
instAction := installAction(t)

@ -38,6 +38,7 @@ type Lint struct {
WithSubcharts bool
}
// LintResult is the result of Lint
type LintResult struct {
TotalChartsLinted int
Messages []support.Message

@ -165,6 +165,10 @@ func (l *List) Run() ([]*release.Release, error) {
return true
})
if err != nil {
return nil, err
}
if results == nil {
return results, nil
}
@ -238,7 +242,7 @@ func filterList(releases []*release.Release) []*release.Release {
return list
}
// setStateMask calculates the state mask based on parameters.
// SetStateMask calculates the state mask based on parameters.
func (l *List) SetStateMask() {
if l.All {
l.StateMask = ListAll

@ -118,6 +118,7 @@ func setVersion(ch *chart.Chart, ver string) error {
return nil
}
// Clearsign signs a chart
func (p *Package) Clearsign(filename string) error {
// Load keyring
signer, err := provenance.NewFromKeyring(p.Keyring, p.Key)

@ -101,7 +101,11 @@ func (p *Pull) Run(chartRef string) (string, error) {
}
if p.Verify {
fmt.Fprintf(&out, "Verification: %v\n", v)
for name := range v.SignedBy.Identities {
fmt.Fprintf(&out, "Signed by: %v\n", name)
}
fmt.Fprintf(&out, "Using Key With Fingerprint: %X\n", v.SignedBy.PrimaryKey.Fingerprint)
fmt.Fprintf(&out, "Chart Hash Verified: %s\n", v.FileHash)
}
// After verification, untar the chart into the requested directory.

@ -27,12 +27,17 @@ import (
"helm.sh/helm/v3/pkg/chartutil"
)
// ShowOutputFormat is the format of the output of `helm show`
type ShowOutputFormat string
const (
// ShowAll is the format which shows all the information of a chart
ShowAll ShowOutputFormat = "all"
// ShowChart is the format which only shows the chart's definition
ShowChart ShowOutputFormat = "chart"
// ShowValues is the format which only shows the chart's values
ShowValues ShowOutputFormat = "values"
// ShowReadme is the format which only shows the chart's README
ShowReadme ShowOutputFormat = "readme"
)
@ -46,8 +51,9 @@ func (o ShowOutputFormat) String() string {
//
// It provides the implementation of 'helm show' and its respective subcommands.
type Show struct {
OutputFormat ShowOutputFormat
ChartPathOptions
Devel bool
OutputFormat ShowOutputFormat
}
// NewShow creates a new Show object with the given configuration.

@ -45,6 +45,8 @@ type Upgrade struct {
Install bool
Devel bool
Namespace string
// SkipCRDs skip installing CRDs when install flag is enabled during upgrade
SkipCRDs bool
Timeout time.Duration
Wait bool
DisableHooks bool
@ -61,6 +63,7 @@ type Upgrade struct {
SubNotes bool
Description string
PostRenderer postrender.PostRenderer
DisableOpenAPIValidation bool
}
// NewUpgrade creates a new Upgrade object with the given configuration.
@ -72,6 +75,10 @@ func NewUpgrade(cfg *Configuration) *Upgrade {
// Run executes the upgrade on the given release.
func (u *Upgrade) Run(name string, chart *chart.Chart, vals map[string]interface{}) (*release.Release, error) {
if err := u.cfg.KubeClient.IsReachable(); err != nil {
return nil, err
}
// Make sure if Atomic is set, that wait is set as well. This makes it so
// the user doesn't have to specify both
u.Wait = u.Wait || u.Atomic
@ -184,7 +191,7 @@ func (u *Upgrade) prepareUpgrade(name string, chart *chart.Chart, vals map[strin
if len(notesTxt) > 0 {
upgradedRelease.Info.Notes = notesTxt
}
err = validateManifest(u.cfg.KubeClient, manifestDoc.Bytes())
err = validateManifest(u.cfg.KubeClient, manifestDoc.Bytes(), !u.DisableOpenAPIValidation)
return currentRelease, upgradedRelease, err
}
@ -193,11 +200,17 @@ func (u *Upgrade) performUpgrade(originalRelease, upgradedRelease *release.Relea
if err != nil {
return upgradedRelease, errors.Wrap(err, "unable to build kubernetes objects from current release manifest")
}
target, err := u.cfg.KubeClient.Build(bytes.NewBufferString(upgradedRelease.Manifest), true)
target, err := u.cfg.KubeClient.Build(bytes.NewBufferString(upgradedRelease.Manifest), !u.DisableOpenAPIValidation)
if err != nil {
return upgradedRelease, errors.Wrap(err, "unable to build kubernetes objects from new release manifest")
}
// It is safe to use force only on target because these are resources currently rendered by the chart.
err = target.Visit(setMetadataVisitor(upgradedRelease.Name, upgradedRelease.Namespace, true))
if err != nil {
return upgradedRelease, err
}
// Do a basic diff using gvk + name to figure out what new resources are being created so we can validate they don't already exist
existingResources := make(map[string]bool)
for _, r := range current {
@ -211,10 +224,19 @@ func (u *Upgrade) performUpgrade(originalRelease, upgradedRelease *release.Relea
}
}
if err := existingResourceConflict(toBeCreated); err != nil {
return nil, errors.Wrap(err, "rendered manifests contain a new resource that already exists. Unable to continue with update")
toBeUpdated, err := existingResourceConflict(toBeCreated, upgradedRelease.Name, upgradedRelease.Namespace)
if err != nil {
return nil, errors.Wrap(err, "rendered manifests contain a resource that already exists. Unable to continue with update")
}
toBeUpdated.Visit(func(r *resource.Info, err error) error {
if err != nil {
return err
}
current.Append(r)
return nil
})
if u.DryRun {
u.cfg.Log("dry run for %s", upgradedRelease.Name)
if len(u.Description) > 0 {
@ -379,8 +401,8 @@ func (u *Upgrade) reuseValues(chart *chart.Chart, current *release.Release, newV
return newVals, nil
}
func validateManifest(c kube.Interface, manifest []byte) error {
_, err := c.Build(bytes.NewReader(manifest), true)
func validateManifest(c kube.Interface, manifest []byte, openAPIValidation bool) error {
_, err := c.Build(bytes.NewReader(manifest), openAPIValidation)
return err
}

@ -21,12 +21,25 @@ import (
"github.com/pkg/errors"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/api/meta"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/cli-runtime/pkg/resource"
"helm.sh/helm/v3/pkg/kube"
)
func existingResourceConflict(resources kube.ResourceList) error {
var accessor = meta.NewAccessor()
const (
appManagedByLabel = "app.kubernetes.io/managed-by"
appManagedByHelm = "Helm"
helmReleaseNameAnnotation = "meta.helm.sh/release-name"
helmReleaseNamespaceAnnotation = "meta.helm.sh/release-namespace"
)
func existingResourceConflict(resources kube.ResourceList, releaseName, releaseNamespace string) (kube.ResourceList, error) {
var requireUpdate kube.ResourceList
err := resources.Visit(func(info *resource.Info, err error) error {
if err != nil {
return err
@ -38,11 +51,134 @@ func existingResourceConflict(resources kube.ResourceList) error {
if apierrors.IsNotFound(err) {
return nil
}
return errors.Wrap(err, "could not get information about the resource")
}
return fmt.Errorf("existing resource conflict: namespace: %s, name: %s, existing_kind: %s, new_kind: %s", info.Namespace, info.Name, existing.GetObjectKind().GroupVersionKind(), info.Mapping.GroupVersionKind)
// Allow adoption of the resource if it is managed by Helm and is annotated with correct release name and namespace.
if err := checkOwnership(existing, releaseName, releaseNamespace); err != nil {
return fmt.Errorf("%s exists and cannot be imported into the current release: %s", resourceString(info), err)
}
requireUpdate.Append(info)
return nil
})
return requireUpdate, err
}
func checkOwnership(obj runtime.Object, releaseName, releaseNamespace string) error {
lbls, err := accessor.Labels(obj)
if err != nil {
return err
}
annos, err := accessor.Annotations(obj)
if err != nil {
return err
}
var errs []error
if err := requireValue(lbls, appManagedByLabel, appManagedByHelm); err != nil {
errs = append(errs, fmt.Errorf("label validation error: %s", err))
}
if err := requireValue(annos, helmReleaseNameAnnotation, releaseName); err != nil {
errs = append(errs, fmt.Errorf("annotation validation error: %s", err))
}
if err := requireValue(annos, helmReleaseNamespaceAnnotation, releaseNamespace); err != nil {
errs = append(errs, fmt.Errorf("annotation validation error: %s", err))
}
if len(errs) > 0 {
err := errors.New("invalid ownership metadata")
for _, e := range errs {
err = fmt.Errorf("%w; %s", err, e)
}
return err
}
return nil
}
func requireValue(meta map[string]string, k, v string) error {
actual, ok := meta[k]
if !ok {
return fmt.Errorf("missing key %q: must be set to %q", k, v)
}
if actual != v {
return fmt.Errorf("key %q must equal %q: current value is %q", k, v, actual)
}
return nil
}
// setMetadataVisitor adds release tracking metadata to all resources. If force is enabled, existing
// ownership metadata will be overwritten. Otherwise an error will be returned if any resource has an
// existing and conflicting value for the managed by label or Helm release/namespace annotations.
func setMetadataVisitor(releaseName, releaseNamespace string, force bool) resource.VisitorFunc {
return func(info *resource.Info, err error) error {
if err != nil {
return err
}
if !force {
if err := checkOwnership(info.Object, releaseName, releaseNamespace); err != nil {
return fmt.Errorf("%s cannot be owned: %s", resourceString(info), err)
}
}
if err := mergeLabels(info.Object, map[string]string{
appManagedByLabel: appManagedByHelm,
}); err != nil {
return fmt.Errorf(
"%s labels could not be updated: %s",
resourceString(info), err,
)
}
if err := mergeAnnotations(info.Object, map[string]string{
helmReleaseNameAnnotation: releaseName,
helmReleaseNamespaceAnnotation: releaseNamespace,
}); err != nil {
return fmt.Errorf(
"%s annotations could not be updated: %s",
resourceString(info), err,
)
}
return nil
}
}
func resourceString(info *resource.Info) string {
_, k := info.Mapping.GroupVersionKind.ToAPIVersionAndKind()
return fmt.Sprintf(
"%s %q in namespace %q",
k, info.Name, info.Namespace,
)
}
func mergeLabels(obj runtime.Object, labels map[string]string) error {
current, err := accessor.Labels(obj)
if err != nil {
return err
}
return accessor.SetLabels(obj, mergeStrStrMaps(current, labels))
}
func mergeAnnotations(obj runtime.Object, annotations map[string]string) error {
current, err := accessor.Annotations(obj)
if err != nil {
return err
}
return accessor.SetAnnotations(obj, mergeStrStrMaps(current, annotations))
}
// merge two maps, always taking the value on the right
func mergeStrStrMaps(current, desired map[string]string) map[string]string {
result := make(map[string]string)
for k, v := range current {
result[k] = v
}
for k, desiredVal := range desired {
result[k] = desiredVal
}
return result
}

@ -0,0 +1,123 @@
/*
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 action
import (
"testing"
"helm.sh/helm/v3/pkg/kube"
appsv1 "k8s.io/api/apps/v1"
"github.com/stretchr/testify/assert"
"k8s.io/apimachinery/pkg/api/meta"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/cli-runtime/pkg/resource"
)
func newDeploymentResource(name, namespace string) *resource.Info {
return &resource.Info{
Name: name,
Mapping: &meta.RESTMapping{
Resource: schema.GroupVersionResource{Group: "apps", Version: "v1", Resource: "deployment"},
GroupVersionKind: schema.GroupVersionKind{Group: "apps", Version: "v1", Kind: "Deployment"},
},
Object: &appsv1.Deployment{
ObjectMeta: v1.ObjectMeta{
Name: name,
Namespace: namespace,
},
},
}
}
func TestCheckOwnership(t *testing.T) {
deployFoo := newDeploymentResource("foo", "ns-a")
// Verify that a resource that lacks labels/annotations is not owned
err := checkOwnership(deployFoo.Object, "rel-a", "ns-a")
assert.EqualError(t, err, `invalid ownership metadata; label validation error: missing key "app.kubernetes.io/managed-by": must be set to "Helm"; annotation validation error: missing key "meta.helm.sh/release-name": must be set to "rel-a"; annotation validation error: missing key "meta.helm.sh/release-namespace": must be set to "ns-a"`)
// Set managed by label and verify annotation error message
_ = accessor.SetLabels(deployFoo.Object, map[string]string{
appManagedByLabel: appManagedByHelm,
})
err = checkOwnership(deployFoo.Object, "rel-a", "ns-a")
assert.EqualError(t, err, `invalid ownership metadata; annotation validation error: missing key "meta.helm.sh/release-name": must be set to "rel-a"; annotation validation error: missing key "meta.helm.sh/release-namespace": must be set to "ns-a"`)
// Set only the release name annotation and verify missing release namespace error message
_ = accessor.SetAnnotations(deployFoo.Object, map[string]string{
helmReleaseNameAnnotation: "rel-a",
})
err = checkOwnership(deployFoo.Object, "rel-a", "ns-a")
assert.EqualError(t, err, `invalid ownership metadata; annotation validation error: missing key "meta.helm.sh/release-namespace": must be set to "ns-a"`)
// Set both release name and namespace annotations and verify no ownership errors
_ = accessor.SetAnnotations(deployFoo.Object, map[string]string{
helmReleaseNameAnnotation: "rel-a",
helmReleaseNamespaceAnnotation: "ns-a",
})
err = checkOwnership(deployFoo.Object, "rel-a", "ns-a")
assert.NoError(t, err)
// Verify ownership error for wrong release name
err = checkOwnership(deployFoo.Object, "rel-b", "ns-a")
assert.EqualError(t, err, `invalid ownership metadata; annotation validation error: key "meta.helm.sh/release-name" must equal "rel-b": current value is "rel-a"`)
// Verify ownership error for wrong release namespace
err = checkOwnership(deployFoo.Object, "rel-a", "ns-b")
assert.EqualError(t, err, `invalid ownership metadata; annotation validation error: key "meta.helm.sh/release-namespace" must equal "ns-b": current value is "ns-a"`)
// Verify ownership error for wrong manager label
_ = accessor.SetLabels(deployFoo.Object, map[string]string{
appManagedByLabel: "helm",
})
err = checkOwnership(deployFoo.Object, "rel-a", "ns-a")
assert.EqualError(t, err, `invalid ownership metadata; label validation error: key "app.kubernetes.io/managed-by" must equal "Helm": current value is "helm"`)
}
func TestSetMetadataVisitor(t *testing.T) {
var (
err error
deployFoo = newDeploymentResource("foo", "ns-a")
deployBar = newDeploymentResource("bar", "ns-a-system")
resources = kube.ResourceList{deployFoo, deployBar}
)
// Set release tracking metadata and verify no error
err = resources.Visit(setMetadataVisitor("rel-a", "ns-a", true))
assert.NoError(t, err)
// Verify that release "b" cannot take ownership of "a"
err = resources.Visit(setMetadataVisitor("rel-b", "ns-a", false))
assert.Error(t, err)
// Force release "b" to take ownership
err = resources.Visit(setMetadataVisitor("rel-b", "ns-a", true))
assert.NoError(t, err)
// Check that there is now no ownership error when setting metadata without force
err = resources.Visit(setMetadataVisitor("rel-b", "ns-a", false))
assert.NoError(t, err)
// Add a new resource that is missing ownership metadata and verify error
resources.Append(newDeploymentResource("baz", "default"))
err = resources.Visit(setMetadataVisitor("rel-b", "ns-a", false))
assert.Error(t, err)
assert.Contains(t, err.Error(), `Deployment "baz" in namespace "" cannot be owned`)
}

@ -17,6 +17,9 @@ limitations under the License.
package action
import (
"fmt"
"strings"
"helm.sh/helm/v3/pkg/downloader"
)
@ -25,6 +28,7 @@ import (
// It provides the implementation of 'helm verify'.
type Verify struct {
Keyring string
Out string
}
// NewVerify creates a new Verify object with the given configuration.
@ -34,6 +38,22 @@ func NewVerify() *Verify {
// Run executes 'helm verify'.
func (v *Verify) Run(chartfile string) error {
_, err := downloader.VerifyChart(chartfile, v.Keyring)
var out strings.Builder
p, err := downloader.VerifyChart(chartfile, v.Keyring)
if err != nil {
return err
}
for name := range p.SignedBy.Identities {
fmt.Fprintf(&out, "Signed by: %v\n", name)
}
fmt.Fprintf(&out, "Using Key With Fingerprint: %X\n", p.SignedBy.PrimaryKey.Fingerprint)
fmt.Fprintf(&out, "Chart Hash Verified: %s\n", p.FileHash)
// TODO(mattfarina): The output is set as a property rather than returned
// to maintain the Go API. In Helm v4 this function should return the out
// and the property on the struct can be removed.
v.Out = out.String()
return nil
}

@ -165,6 +165,17 @@ Loop:
cd = append(cd, n)
}
}
// don't keep disabled charts in metadata
cdMetadata := []*chart.Dependency{}
copy(cdMetadata, c.Metadata.Dependencies[:0])
for _, n := range c.Metadata.Dependencies {
if _, ok := rm[n.Name]; !ok {
cdMetadata = append(cdMetadata, n)
}
}
// set the correct dependencies in metadata
c.Metadata.Dependencies = nil
c.Metadata.Dependencies = append(c.Metadata.Dependencies, cdMetadata...)
c.SetDependencies(cd...)
return nil
@ -213,7 +224,7 @@ func processImportValues(c *chart.Chart, cvals Values) error {
// get child table
vv, err := cvals.Table(r.Name + "." + child)
if err != nil {
log.Printf("Warning: ImportValues missing table: %v", err)
log.Printf("Warning: ImportValues missing table from chart %s: %v", r.Name, err)
continue
}
// create value map from child to be merged into parent

@ -273,6 +273,36 @@ func TestProcessDependencyImportValues(t *testing.T) {
}
}
func TestProcessDependencyImportValuesForEnabledCharts(t *testing.T) {
c := loadChart(t, "testdata/import-values-from-enabled-subchart/parent-chart")
nameOverride := "parent-chart-prod"
if err := processDependencyImportValues(c); err != nil {
t.Fatalf("processing import values dependencies %v", err)
}
if len(c.Dependencies()) != 2 {
t.Fatalf("expected 2 dependencies for this chart, but got %d", len(c.Dependencies()))
}
if err := processDependencyEnabled(c, c.Values, ""); err != nil {
t.Fatalf("expected no errors but got %q", err)
}
if len(c.Dependencies()) != 1 {
t.Fatal("expected no changes in dependencies")
}
if len(c.Metadata.Dependencies) != 1 {
t.Fatalf("expected 1 dependency specified in Chart.yaml, got %d", len(c.Metadata.Dependencies))
}
prodDependencyValues := c.Dependencies()[0].Values
if prodDependencyValues["nameOverride"] != nameOverride {
t.Fatalf("dependency chart name should be %s but got %s", nameOverride, prodDependencyValues["nameOverride"])
}
}
func TestGetAliasDependency(t *testing.T) {
c := loadChart(t, "testdata/frobnitz")
req := c.Metadata.Dependencies

@ -39,19 +39,6 @@ func TestExpand(t *testing.T) {
t.Fatal(err)
}
files, err := ioutil.ReadDir(dest)
if err != nil {
t.Fatalf("error reading output directory %s: %s", dest, err)
}
if len(files) != 1 {
t.Fatalf("expected a single chart directory in output directory %s", dest)
}
if !files[0].IsDir() {
t.Fatalf("expected a chart directory in output directory %s", dest)
}
expectedChartPath := filepath.Join(dest, "frobnitz")
fi, err := os.Stat(expectedChartPath)
if err != nil {
@ -81,10 +68,16 @@ func TestExpand(t *testing.T) {
if err != nil {
t.Fatal(err)
}
// os.Stat can return different values for directories, based on the OS
// for Linux, for example, os.Stat alwaty returns the size of the directory
// (value-4096) regardless of the size of the contents of the directory
mode := expect.Mode()
if !mode.IsDir() {
if fi.Size() != expect.Size() {
t.Errorf("Expected %s to have size %d, got %d", fi.Name(), expect.Size(), fi.Size())
}
}
}
}
func TestExpandFile(t *testing.T) {
@ -127,8 +120,14 @@ func TestExpandFile(t *testing.T) {
if err != nil {
t.Fatal(err)
}
// os.Stat can return different values for directories, based on the OS
// for Linux, for example, os.Stat alwaty returns the size of the directory
// (value-4096) regardless of the size of the contents of the directory
mode := expect.Mode()
if !mode.IsDir() {
if fi.Size() != expect.Size() {
t.Errorf("Expected %s to have size %d, got %d", fi.Name(), expect.Size(), fi.Size())
}
}
}
}

@ -161,6 +161,20 @@ func writeTarContents(out *tar.Writer, c *chart.Chart, prefix string) error {
return err
}
// Save Chart.lock
// TODO: remove the APIVersion check when APIVersionV1 is not used anymore
if c.Metadata.APIVersion == chart.APIVersionV2 {
if c.Lock != nil {
ldata, err := yaml.Marshal(c.Lock)
if err != nil {
return err
}
if err := writeToTar(out, filepath.Join(base, "Chart.lock"), ldata); err != nil {
return err
}
}
}
// Save values.yaml
for _, f := range c.Raw {
if f.Name == ValuesfileName {
@ -217,7 +231,7 @@ func writeTarContents(out *tar.Writer, c *chart.Chart, prefix string) error {
func writeToTar(out *tar.Writer, name string, body []byte) error {
// TODO: Do we need to create dummy parent directory names if none exist?
h := &tar.Header{
Name: name,
Name: filepath.ToSlash(name),
Mode: 0644,
Size: int64(len(body)),
ModTime: time.Now(),

@ -49,6 +49,9 @@ func TestSave(t *testing.T) {
Name: "ahab",
Version: "1.2.3",
},
Lock: &chart.Lock{
Digest: "testdigest",
},
Files: []*chart.File{
{Name: "scheherazade/shahryar.txt", Data: []byte("1,001 Nights")},
},
@ -77,6 +80,9 @@ func TestSave(t *testing.T) {
if len(c2.Files) != 1 || c2.Files[0].Name != "scheherazade/shahryar.txt" {
t.Fatal("Files data did not match")
}
if c2.Lock != nil {
t.Fatal("Expected v1 chart archive not to contain Chart.lock file")
}
if !bytes.Equal(c.Schema, c2.Schema) {
indentation := 4
@ -87,6 +93,22 @@ func TestSave(t *testing.T) {
if _, err := Save(&chartWithInvalidJSON, dest); err == nil {
t.Fatalf("Invalid JSON was not caught while saving chart")
}
c.Metadata.APIVersion = chart.APIVersionV2
where, err = Save(c, dest)
if err != nil {
t.Fatalf("Failed to save: %s", err)
}
c2, err = loader.LoadFile(where)
if err != nil {
t.Fatal(err)
}
if c2.Lock == nil {
t.Fatal("Expected v2 chart archive to containe a Chart.lock file")
}
if c2.Lock.Digest != c.Lock.Digest {
t.Fatal("Chart.lock data did not match")
}
})
}
}

@ -0,0 +1,9 @@
dependencies:
- name: dev
repository: file://envs/dev
version: v0.1.0
- name: prod
repository: file://envs/prod
version: v0.1.0
digest: sha256:9403fc24f6cf9d6055820126cf7633b4bd1fed3c77e4880c674059f536346182
generated: "2020-02-03T10:38:51.180474+01:00"

@ -0,0 +1,22 @@
apiVersion: v2
name: parent-chart
version: v0.1.0
appVersion: v0.1.0
dependencies:
- name: dev
repository: "file://envs/dev"
version: ">= 0.0.1"
condition: dev.enabled,global.dev.enabled
tags:
- dev
import-values:
- data
- name: prod
repository: "file://envs/prod"
version: ">= 0.0.1"
condition: prod.enabled,global.prod.enabled
tags:
- prod
import-values:
- data

@ -0,0 +1,4 @@
apiVersion: v2
name: dev
version: v0.1.0
appVersion: v0.1.0

@ -0,0 +1,9 @@
# Dev values parent-chart
nameOverride: parent-chart-dev
exports:
data:
resources:
autoscaler:
minReplicas: 1
maxReplicas: 3
targetCPUUtilizationPercentage: 80

@ -0,0 +1,4 @@
apiVersion: v2
name: prod
version: v0.1.0
appVersion: v0.1.0

@ -0,0 +1,9 @@
# Prod values parent-chart
nameOverride: parent-chart-prod
exports:
data:
resources:
autoscaler:
minReplicas: 2
maxReplicas: 5
targetCPUUtilizationPercentage: 90

@ -0,0 +1,16 @@
###################################################################################################
# parent-chart horizontal pod autoscaler
###################################################################################################
apiVersion: autoscaling/v1
kind: HorizontalPodAutoscaler
metadata:
name: {{ .Release.Name }}-autoscaler
namespace: {{ .Release.Namespace }}
spec:
scaleTargetRef:
apiVersion: apps/v1beta1
kind: Deployment
name: {{ .Release.Name }}
minReplicas: {{ required "A valid .Values.resources.autoscaler.minReplicas entry required!" .Values.resources.autoscaler.minReplicas }}
maxReplicas: {{ required "A valid .Values.resources.autoscaler.maxReplicas entry required!" .Values.resources.autoscaler.maxReplicas }}
targetCPUUtilizationPercentage: {{ required "A valid .Values.resources.autoscaler.targetCPUUtilizationPercentage!" .Values.resources.autoscaler.targetCPUUtilizationPercentage }}

@ -0,0 +1,10 @@
# Default values for parent-chart.
nameOverride: parent-chart
tags:
dev: false
prod: true
resources:
autoscaler:
minReplicas: 0
maxReplicas: 0
targetCPUUtilizationPercentage: 99

@ -46,6 +46,10 @@ type EnvSettings struct {
KubeConfig string
// KubeContext is the name of the kubeconfig context.
KubeContext string
// Bearer KubeToken used for authentication
KubeToken string
// Kubernetes API Server Endpoint for authentication
KubeAPIServer string
// Debug indicates whether or not Helm is running in Debug mode.
Debug bool
// RegistryConfig is the path to the registry config file.
@ -63,6 +67,8 @@ func New() *EnvSettings {
env := EnvSettings{
namespace: os.Getenv("HELM_NAMESPACE"),
KubeContext: os.Getenv("HELM_KUBECONTEXT"),
KubeToken: os.Getenv("HELM_KUBETOKEN"),
KubeAPIServer: os.Getenv("HELM_KUBEAPISERVER"),
PluginsDirectory: envOr("HELM_PLUGINS", helmpath.DataPath("plugins")),
RegistryConfig: envOr("HELM_REGISTRY_CONFIG", helmpath.ConfigPath("registry.json")),
RepositoryConfig: envOr("HELM_REPOSITORY_CONFIG", helmpath.ConfigPath("repositories.yaml")),
@ -77,6 +83,8 @@ func (s *EnvSettings) AddFlags(fs *pflag.FlagSet) {
fs.StringVarP(&s.namespace, "namespace", "n", s.namespace, "namespace scope for this request")
fs.StringVar(&s.KubeConfig, "kubeconfig", "", "path to the kubeconfig file")
fs.StringVar(&s.KubeContext, "kube-context", s.KubeContext, "name of the kubeconfig context to use")
fs.StringVar(&s.KubeToken, "kube-token", s.KubeToken, "bearer token used for authentication")
fs.StringVar(&s.KubeAPIServer, "kube-apiserver", s.KubeAPIServer, "the address and the port for the Kubernetes API server")
fs.BoolVar(&s.Debug, "debug", s.Debug, "enable verbose output")
fs.StringVar(&s.RegistryConfig, "registry-config", s.RegistryConfig, "path to the registry config file")
fs.StringVar(&s.RepositoryConfig, "repository-config", s.RepositoryConfig, "path to the file containing repository names and URLs")
@ -100,6 +108,8 @@ func (s *EnvSettings) EnvVars() map[string]string {
"HELM_REPOSITORY_CONFIG": s.RepositoryConfig,
"HELM_NAMESPACE": s.Namespace(),
"HELM_KUBECONTEXT": s.KubeContext,
"HELM_KUBETOKEN": s.KubeToken,
"HELM_KUBEAPISERVER": s.KubeAPIServer,
}
if s.KubeConfig != "" {
@ -124,7 +134,15 @@ func (s *EnvSettings) Namespace() string {
//RESTClientGetter gets the kubeconfig from EnvSettings
func (s *EnvSettings) RESTClientGetter() genericclioptions.RESTClientGetter {
s.configOnce.Do(func() {
s.config = kube.GetConfig(s.KubeConfig, s.KubeContext, s.namespace)
clientConfig := kube.GetConfig(s.KubeConfig, s.KubeContext, s.namespace)
if s.KubeToken != "" {
clientConfig.BearerToken = &s.KubeToken
}
if s.KubeAPIServer != "" {
clientConfig.APIServer = &s.KubeAPIServer
}
s.config = clientConfig
})
return s.config
}

@ -181,8 +181,10 @@ func (c *ChartDownloader) ResolveChartVersion(ref, version string) (*url.URL, er
c.Options = append(
c.Options,
getter.WithURL(rc.URL),
getter.WithTLSClientConfig(rc.CertFile, rc.KeyFile, rc.CAFile),
)
if rc.CertFile != "" || rc.KeyFile != "" || rc.CAFile != "" {
c.Options = append(c.Options, getter.WithTLSClientConfig(rc.CertFile, rc.KeyFile, rc.CAFile))
}
if rc.Username != "" && rc.Password != "" {
c.Options = append(
c.Options,
@ -210,13 +212,15 @@ func (c *ChartDownloader) ResolveChartVersion(ref, version string) (*url.URL, er
if err != nil {
return u, err
}
if r != nil && r.Config != nil && r.Config.Username != "" && r.Config.Password != "" {
c.Options = append(c.Options, getter.WithBasicAuth(r.Config.Username, r.Config.Password))
}
if r != nil && r.Config != nil {
if r.Config.CertFile != "" || r.Config.KeyFile != "" || r.Config.CAFile != "" {
c.Options = append(c.Options, getter.WithTLSClientConfig(r.Config.CertFile, r.Config.KeyFile, r.Config.CAFile))
}
if r.Config.Username != "" && r.Config.Password != "" {
c.Options = append(c.Options, getter.WithBasicAuth(r.Config.Username, r.Config.Password))
}
}
// Next, we need to load the index, and actually look up the chart.
idxFile := filepath.Join(c.RepositoryCache, helmpath.CacheIndexFile(r.Config.Name))
@ -255,9 +259,6 @@ func (c *ChartDownloader) ResolveChartVersion(ref, version string) (*url.URL, er
if _, err := getter.NewHTTPGetter(getter.WithURL(rc.URL)); err != nil {
return repoURL, err
}
if r != nil && r.Config != nil && r.Config.Username != "" && r.Config.Password != "" {
c.Options = append(c.Options, getter.WithBasicAuth(r.Config.Username, r.Config.Password))
}
return u, err
}

@ -227,6 +227,58 @@ func TestDownloadTo(t *testing.T) {
}
}
func TestDownloadTo_TLS(t *testing.T) {
// Set up mock server w/ tls enabled
srv, err := repotest.NewTempServer("testdata/*.tgz*")
srv.Stop()
if err != nil {
t.Fatal(err)
}
srv.StartTLS()
defer srv.Stop()
if err := srv.CreateIndex(); err != nil {
t.Fatal(err)
}
if err := srv.LinkIndices(); err != nil {
t.Fatal(err)
}
repoConfig := filepath.Join(srv.Root(), "repositories.yaml")
repoCache := srv.Root()
c := ChartDownloader{
Out: os.Stderr,
Verify: VerifyAlways,
Keyring: "testdata/helm-test-key.pub",
RepositoryConfig: repoConfig,
RepositoryCache: repoCache,
Getters: getter.All(&cli.EnvSettings{
RepositoryConfig: repoConfig,
RepositoryCache: repoCache,
}),
Options: []getter.Option{},
}
cname := "test/signtest"
dest := srv.Root()
where, v, err := c.DownloadTo(cname, "", dest)
if err != nil {
t.Fatal(err)
}
target := filepath.Join(dest, "signtest-0.1.0.tgz")
if expect := target; where != expect {
t.Errorf("Expected download to %s, got %s", expect, where)
}
if v.FileHash == "" {
t.Error("File hash was empty, but verification is required.")
}
if _, err := os.Stat(target); err != nil {
t.Error(err)
}
}
func TestDownloadTo_VerifyLater(t *testing.T) {
defer ensure.HelmHome(t)()

@ -82,6 +82,19 @@ func (m *Manager) Build() error {
// Check that all of the repos we're dependent on actually exist.
req := c.Metadata.Dependencies
// If using apiVersion v1, calculate the hash before resolve repo names
// because resolveRepoNames will change req if req uses repo alias
// and Helm 2 calculate the digest from the original req
// Fix for: https://github.com/helm/helm/issues/7619
var v2Sum string
if c.Metadata.APIVersion == chart.APIVersionV1 {
v2Sum, err = resolver.HashV2Req(req)
if err != nil {
return errors.New("the lock file (requirements.lock) is out of sync with the dependencies file (requirements.yaml). Please update the dependencies")
}
}
if _, err := m.resolveRepoNames(req); err != nil {
return err
}
@ -92,7 +105,7 @@ func (m *Manager) Build() error {
// Fix for: https://github.com/helm/helm/issues/7233
if c.Metadata.APIVersion == chart.APIVersionV1 {
log.Println("warning: a valid Helm v3 hash was not found. Checking against Helm v2 hash...")
if sum, err := resolver.HashV2Req(req); err != nil || sum != lock.Digest {
if v2Sum != lock.Digest {
return errors.New("the lock file (requirements.lock) is out of sync with the dependencies file (requirements.yaml). Please update the dependencies")
}
} else {

@ -45,3 +45,12 @@ func TestError(t *testing.T) {
t.Errorf("incorrect error message. Received %s", g.Error().Error())
}
}
func TestString(t *testing.T) {
os.Unsetenv(name)
g := Gate(name)
if g.String() != "HELM_EXPERIMENTAL_FEATURE" {
t.Errorf("incorrect string representation. Received %s", g.String())
}
}

@ -32,6 +32,7 @@ type options struct {
certFile string
keyFile string
caFile string
insecureSkipVerifyTLS bool
username string
password string
userAgent string
@ -64,6 +65,13 @@ func WithUserAgent(userAgent string) Option {
}
}
// WithInsecureSkipVerifyTLS determines if a TLS Certificate will be checked
func WithInsecureSkipVerifyTLS(insecureSkipVerifyTLS bool) Option {
return func(opts *options) {
opts.insecureSkipVerifyTLS = insecureSkipVerifyTLS
}
}
// WithTLSClientConfig sets the client auth with the provided credentials.
func WithTLSClientConfig(certFile, keyFile, caFile string) Option {
return func(opts *options) {

@ -17,6 +17,7 @@ package getter
import (
"bytes"
"crypto/tls"
"io"
"net/http"
@ -111,5 +112,19 @@ func (g *HTTPGetter) httpClient() (*http.Client, error) {
return client, nil
}
if g.opts.insecureSkipVerifyTLS {
client := &http.Client{
Transport: &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true,
},
Proxy: http.ProxyFromEnvironment,
},
}
return client, nil
}
return http.DefaultClient, nil
}

@ -44,12 +44,14 @@ func TestHTTPGetter(t *testing.T) {
cd := "../../testdata"
join := filepath.Join
ca, pub, priv := join(cd, "rootca.crt"), join(cd, "crt.pem"), join(cd, "key.pem")
insecure := false
// Test with options
g, err = NewHTTPGetter(
WithBasicAuth("I", "Am"),
WithUserAgent("Groot"),
WithTLSClientConfig(pub, priv, ca),
WithInsecureSkipVerifyTLS(insecure),
)
if err != nil {
t.Fatal(err)
@ -83,6 +85,29 @@ func TestHTTPGetter(t *testing.T) {
if hg.opts.caFile != ca {
t.Errorf("Expected NewHTTPGetter to contain %q as the CA file, got %q", ca, hg.opts.caFile)
}
if hg.opts.insecureSkipVerifyTLS != insecure {
t.Errorf("Expected NewHTTPGetter to contain %t as InsecureSkipVerifyTLs flag, got %t", false, hg.opts.insecureSkipVerifyTLS)
}
// Test if setting insecureSkipVerifyTLS is being passed to the ops
insecure = true
g, err = NewHTTPGetter(
WithInsecureSkipVerifyTLS(insecure),
)
if err != nil {
t.Fatal(err)
}
hg, ok = g.(*HTTPGetter)
if !ok {
t.Fatal("expected NewHTTPGetter to produce an *HTTPGetter")
}
if hg.opts.insecureSkipVerifyTLS != insecure {
t.Errorf("Expected NewHTTPGetter to contain %t as InsecureSkipVerifyTLs flag, got %t", insecure, hg.opts.insecureSkipVerifyTLS)
}
}
func TestDownload(t *testing.T) {
@ -191,3 +216,35 @@ func TestDownloadTLS(t *testing.T) {
t.Error(err)
}
}
func TestDownloadInsecureSkipTLSVerify(t *testing.T) {
ts := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}))
defer ts.Close()
u, _ := url.ParseRequestURI(ts.URL)
// Ensure the default behaviour did not change
g, err := NewHTTPGetter(
WithURL(u.String()),
)
if err != nil {
t.Error(err)
}
if _, err := g.Get(u.String()); err == nil {
t.Errorf("Expected Getter to throw an error, got %s", err)
}
// Test certificate check skip
g, err = NewHTTPGetter(
WithURL(u.String()),
WithInsecureSkipVerifyTLS(true),
)
if err != nil {
t.Error(err)
}
if _, err = g.Get(u.String()); err != nil {
t.Error(err)
}
}

@ -23,9 +23,9 @@ import (
)
func TestHelmHome(t *testing.T) {
os.Setenv(xdg.XDGCacheHomeEnvVar, "c:\\")
os.Setenv(xdg.XDGConfigHomeEnvVar, "d:\\")
os.Setenv(xdg.XDGDataHomeEnvVar, "e:\\")
os.Setenv(xdg.CacheHomeEnvVar, "c:\\")
os.Setenv(xdg.ConfigHomeEnvVar, "d:\\")
os.Setenv(xdg.DataHomeEnvVar, "e:\\")
isEq := func(t *testing.T, a, b string) {
if a != b {
t.Errorf("Expected %q, got %q", b, a)

@ -32,7 +32,7 @@ const (
)
func TestDataPath(t *testing.T) {
os.Unsetenv(DataHomeEnvVar)
os.Unsetenv(xdg.DataHomeEnvVar)
os.Setenv("APPDATA", filepath.Join(homedir.HomeDir(), "foo"))
expected := filepath.Join(homedir.HomeDir(), "foo", appName, testFile)
@ -41,7 +41,7 @@ func TestDataPath(t *testing.T) {
t.Errorf("expected '%s', got '%s'", expected, lazy.dataPath(testFile))
}
os.Setenv(DataHomeEnvVar, filepath.Join(homedir.HomeDir(), "xdg"))
os.Setenv(xdg.DataHomeEnvVar, filepath.Join(homedir.HomeDir(), "xdg"))
expected = filepath.Join(homedir.HomeDir(), "xdg", appName, testFile)
@ -70,8 +70,8 @@ func TestConfigPath(t *testing.T) {
}
func TestCachePath(t *testing.T) {
os.Unsetenv(CacheHomeEnvVar)
os.Setenv("APPDATA", filepath.Join(homedir.HomeDir(), "foo"))
os.Unsetenv(xdg.CacheHomeEnvVar)
os.Setenv("TEMP", filepath.Join(homedir.HomeDir(), "foo"))
expected := filepath.Join(homedir.HomeDir(), "foo", appName, testFile)
@ -79,7 +79,7 @@ func TestCachePath(t *testing.T) {
t.Errorf("expected '%s', got '%s'", expected, lazy.cachePath(testFile))
}
os.Setenv(CacheHomeEnvVar, filepath.Join(homedir.HomeDir(), "xdg"))
os.Setenv(xdg.CacheHomeEnvVar, filepath.Join(homedir.HomeDir(), "xdg"))
expected = filepath.Join(homedir.HomeDir(), "xdg", appName, testFile)

@ -33,6 +33,7 @@ import (
apiextv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/fields"
"k8s.io/apimachinery/pkg/runtime"
@ -50,6 +51,8 @@ import (
// ErrNoObjectsVisited indicates that during a visit operation, no matching objects were found.
var ErrNoObjectsVisited = errors.New("no objects visited")
var metadataAccessor = meta.NewAccessor()
// Client represents a client capable of communicating with the Kubernetes API.
type Client struct {
Factory Factory
@ -88,7 +91,7 @@ func (c *Client) IsReachable() error {
client, _ := c.Factory.KubernetesClientSet()
_, err := client.ServerVersion()
if err != nil {
return errors.New("Kubernetes cluster unreachable")
return fmt.Errorf("Kubernetes cluster unreachable: %s", err.Error())
}
return nil
}
@ -209,12 +212,20 @@ func (c *Client) Update(original, target ResourceList, force bool) (*Result, err
}
for _, info := range original.Difference(target) {
if info.Mapping.GroupVersionKind.Kind == "CustomResourceDefinition" {
c.Log("Skipping the deletion of CustomResourceDefinition %q", info.Name)
c.Log("Deleting %q in %s...", info.Name, info.Namespace)
if err := info.Get(); err != nil {
c.Log("Unable to get obj %q, err: %s", info.Name, err)
}
annotations, err := metadataAccessor.Annotations(info.Object)
if err != nil {
c.Log("Unable to get annotations on %q, err: %s", info.Name, err)
}
if annotations != nil && annotations[ResourcePolicyAnno] == KeepPolicy {
c.Log("Skipping delete of %q due to annotation [%s=%s]", info.Name, ResourcePolicyAnno, KeepPolicy)
continue
}
c.Log("Deleting %q in %s...", info.Name, info.Namespace)
res.Deleted = append(res.Deleted, info)
if err := deleteResource(info); err != nil {
if apierrors.IsNotFound(err) {
@ -236,11 +247,6 @@ func (c *Client) Delete(resources ResourceList) (*Result, []error) {
var errs []error
res := &Result{}
err := perform(resources, func(info *resource.Info) error {
if info.Mapping.GroupVersionKind.Kind == "CustomResourceDefinition" {
c.Log("Skipping the deletion of CustomResourceDefinition %q", info.Name)
return nil
}
c.Log("Starting delete for %q %s", info.Name, info.Mapping.GroupVersionKind.Kind)
if err := c.skipIfNotFound(deleteResource(info)); err != nil {
// Collect the error and continue on

@ -147,6 +147,8 @@ func TestUpdate(t *testing.T) {
return newResponse(200, &listB.Items[1])
case p == "/namespaces/default/pods/squid" && m == "DELETE":
return newResponse(200, &listB.Items[1])
case p == "/namespaces/default/pods/squid" && m == "GET":
return newResponse(200, &listB.Items[2])
default:
t.Fatalf("unexpected request: %s %s", req.Method, req.URL.Path)
return nil, nil
@ -184,6 +186,7 @@ func TestUpdate(t *testing.T) {
"/namespaces/default/pods/otter:GET",
"/namespaces/default/pods/dolphin:GET",
"/namespaces/default/pods:POST",
"/namespaces/default/pods/squid:GET",
"/namespaces/default/pods/squid:DELETE",
}
if len(expectedActions) != len(actions) {

@ -17,6 +17,10 @@ limitations under the License.
package kube // import "helm.sh/helm/v3/pkg/kube"
import (
"sync"
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
apiextensionsv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
"k8s.io/apimachinery/pkg/api/meta"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
@ -24,6 +28,9 @@ import (
"k8s.io/client-go/kubernetes/scheme"
)
var k8sNativeScheme *runtime.Scheme
var k8sNativeSchemeOnce sync.Once
// AsVersioned converts the given info into a runtime.Object with the correct
// group and version set
func AsVersioned(info *resource.Info) runtime.Object {
@ -33,12 +40,30 @@ func AsVersioned(info *resource.Info) runtime.Object {
// convertWithMapper converts the given object with the optional provided
// RESTMapping. If no mapping is provided, the default schema versioner is used
func convertWithMapper(obj runtime.Object, mapping *meta.RESTMapping) runtime.Object {
var gv = runtime.GroupVersioner(schema.GroupVersions(scheme.Scheme.PrioritizedVersionsAllGroups()))
s := kubernetesNativeScheme()
var gv = runtime.GroupVersioner(schema.GroupVersions(s.PrioritizedVersionsAllGroups()))
if mapping != nil {
gv = mapping.GroupVersionKind.GroupVersion()
}
if obj, err := runtime.ObjectConvertor(scheme.Scheme).ConvertToVersion(obj, gv); err == nil {
if obj, err := runtime.ObjectConvertor(s).ConvertToVersion(obj, gv); err == nil {
return obj
}
return obj
}
// kubernetesNativeScheme returns a clean *runtime.Scheme with _only_ Kubernetes
// native resources added to it. This is required to break free of custom resources
// that may have been added to scheme.Scheme due to Helm being used as a package in
// combination with e.g. a versioned kube client. If we would not do this, the client
// may attempt to perform e.g. a 3-way-merge strategy patch for custom resources.
func kubernetesNativeScheme() *runtime.Scheme {
k8sNativeSchemeOnce.Do(func() {
k8sNativeScheme = runtime.NewScheme()
scheme.AddToScheme(k8sNativeScheme)
// API extensions are not in the above scheme set,
// and must thus be added separately.
apiextensionsv1beta1.AddToScheme(k8sNativeScheme)
apiextensionsv1.AddToScheme(k8sNativeScheme)
})
return k8sNativeScheme
}

@ -0,0 +1,26 @@
/*
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 kube // import "helm.sh/helm/v3/pkg/kube"
// ResourcePolicyAnno is the annotation name for a resource policy
const ResourcePolicyAnno = "helm.sh/resource-policy"
// KeepPolicy is the resource policy type for keep
//
// This resource policy type allows resources to skip being deleted
// during an uninstallRelease action.
const KeepPolicy = "keep"

@ -188,12 +188,25 @@ func (w *waiter) serviceReady(s *corev1.Service) bool {
}
// Make sure the service is not explicitly set to "None" before checking the IP
if (s.Spec.ClusterIP != corev1.ClusterIPNone && s.Spec.ClusterIP == "") ||
if s.Spec.ClusterIP != corev1.ClusterIPNone && s.Spec.ClusterIP == "" {
w.log("Service does not have cluster IP address: %s/%s", s.GetNamespace(), s.GetName())
return false
}
// This checks if the service has a LoadBalancer and that balancer has an Ingress defined
(s.Spec.Type == corev1.ServiceTypeLoadBalancer && s.Status.LoadBalancer.Ingress == nil) {
w.log("Service does not have IP address: %s/%s", s.GetNamespace(), s.GetName())
if s.Spec.Type == corev1.ServiceTypeLoadBalancer {
// do not wait when at least 1 external IP is set
if len(s.Spec.ExternalIPs) > 0 {
w.log("Service %s/%s has external IP addresses (%v), marking as ready", s.GetNamespace(), s.GetName(), s.Spec.ExternalIPs)
return true
}
if s.Status.LoadBalancer.Ingress == nil {
w.log("Service does not have load balancer ingress IP address: %s/%s", s.GetNamespace(), s.GetName())
return false
}
}
return true
}

@ -97,7 +97,7 @@ func TestBadValues(t *testing.T) {
if len(m) < 1 {
t.Fatalf("All didn't fail with expected errors, got %#v", m)
}
if !strings.Contains(m[0].Err.Error(), "cannot unmarshal") {
if !strings.Contains(m[0].Err.Error(), "unable to parse YAML") {
t.Errorf("All didn't have the error for invalid key format: %s", m[0].Err)
}
}

@ -126,11 +126,9 @@ func Templates(linter *support.Linter, values map[string]interface{}, namespace
// key will be raised as well
err := yaml.Unmarshal([]byte(renderedContent), &yamlStruct)
validYaml := linter.RunLinterRule(support.ErrorSev, path, validateYamlContent(err))
if !validYaml {
continue
}
// If YAML linting fails, we sill progress. So we don't capture the returned state
// on this linter run.
linter.RunLinterRule(support.ErrorSev, path, validateYamlContent(err))
}
}

@ -0,0 +1,37 @@
/*
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 rules
import (
"os"
"path/filepath"
"testing"
)
var (
nonExistingValuesFilePath = filepath.Join("/fake/dir", "values.yaml")
)
func TestValidateValuesYamlNotDirectory(t *testing.T) {
_ = os.Mkdir(nonExistingValuesFilePath, os.ModePerm)
defer os.Remove(nonExistingValuesFilePath)
err := validateValuesFileExistence(nonExistingValuesFilePath)
if err == nil {
t.Errorf("validateValuesFileExistence to return a linter error, got no error")
}
}

@ -79,3 +79,30 @@ func TestSortByRevision(t *testing.T) {
return vi < vj
})
}
func TestReverseSortByName(t *testing.T) {
Reverse(releases, SortByName)
check(t, "ByName", func(i, j int) bool {
ni := releases[i].Name
nj := releases[j].Name
return ni > nj
})
}
func TestReverseSortByDate(t *testing.T) {
Reverse(releases, SortByDate)
check(t, "ByDate", func(i, j int) bool {
ti := releases[i].Info.LastDeployed.Second()
tj := releases[j].Info.LastDeployed.Second()
return ti > tj
})
}
func TestReverseSortByRevision(t *testing.T) {
Reverse(releases, SortByRevision)
check(t, "ByRevision", func(i, j int) bool {
vi := releases[i].Version
vj := releases[j].Version
return vi > vj
})
}

@ -19,8 +19,10 @@ package repo // import "helm.sh/helm/v3/pkg/repo"
import (
"crypto/rand"
"encoding/base64"
"encoding/json"
"fmt"
"io/ioutil"
"log"
"net/url"
"os"
"path"
@ -45,6 +47,7 @@ type Entry struct {
CertFile string `json:"certFile"`
KeyFile string `json:"keyFile"`
CAFile string `json:"caFile"`
InsecureSkipTLSverify bool `json:"insecure_skip_tls_verify"`
}
// ChartRepository represents a chart repository
@ -121,6 +124,7 @@ func (r *ChartRepository) DownloadIndexFile() (string, error) {
// TODO add user-agent
resp, err := r.Client.Get(indexURL,
getter.WithURL(r.Config.URL),
getter.WithInsecureSkipVerifyTLS(r.Config.InsecureSkipTLSverify),
getter.WithTLSClientConfig(r.Config.CertFile, r.Config.KeyFile, r.Config.CAFile),
getter.WithBasicAuth(r.Config.Username, r.Config.Password),
)
@ -271,3 +275,11 @@ func ResolveReferenceURL(baseURL, refURL string) (string, error) {
parsedBaseURL.Path = strings.TrimSuffix(parsedBaseURL.Path, "/") + "/"
return parsedBaseURL.ResolveReference(parsedRefURL).String(), nil
}
func (e *Entry) String() string {
buf, err := json.Marshal(e)
if err != nil {
log.Panic(err)
}
return string(buf)
}

@ -22,6 +22,8 @@ import (
"os"
"path/filepath"
"helm.sh/helm/v3/internal/tlsutil"
"sigs.k8s.io/yaml"
"helm.sh/helm/v3/pkg/repo"
@ -143,6 +145,40 @@ func (s *Server) Start() {
}))
}
func (s *Server) StartTLS() {
cd := "../../testdata"
ca, pub, priv := filepath.Join(cd, "rootca.crt"), filepath.Join(cd, "crt.pem"), filepath.Join(cd, "key.pem")
s.srv = httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if s.middleware != nil {
s.middleware.ServeHTTP(w, r)
}
http.FileServer(http.Dir(s.Root())).ServeHTTP(w, r)
}))
tlsConf, err := tlsutil.NewClientTLS(pub, priv, ca)
if err != nil {
panic(err)
}
tlsConf.BuildNameToCertificate()
tlsConf.ServerName = "helm.sh"
s.srv.TLS = tlsConf
s.srv.StartTLS()
// Set up repositories config with ca file
repoConfig := filepath.Join(s.Root(), "repositories.yaml")
r := repo.NewFile()
r.Add(&repo.Entry{
Name: "test",
URL: s.URL(),
CAFile: filepath.Join("../../testdata", "rootca.crt"),
})
if err := r.WriteFile(repoConfig, 0644); err != nil {
panic(err)
}
}
// Stop stops the server and closes all connections.
//
// It should be called explicitly.

@ -201,11 +201,6 @@ func (cfgmaps *ConfigMaps) Update(key string, rls *rspb.Release) error {
func (cfgmaps *ConfigMaps) Delete(key string) (rls *rspb.Release, err error) {
// fetch the release to check existence
if rls, err = cfgmaps.Get(key); err != nil {
if apierrors.IsNotFound(err) {
return nil, ErrReleaseExists
}
cfgmaps.Log("delete: failed to get release %q: %s", key, err)
return nil, err
}
// delete the release

@ -184,3 +184,34 @@ func TestConfigMapUpdate(t *testing.T) {
t.Errorf("Expected status %s, got status %s", rel.Info.Status.String(), got.Info.Status.String())
}
}
func TestConfigMapDelete(t *testing.T) {
vers := 1
name := "smug-pigeon"
namespace := "default"
key := testKey(name, vers)
rel := releaseStub(name, vers, namespace, rspb.StatusDeployed)
cfgmaps := newTestFixtureCfgMaps(t, []*rspb.Release{rel}...)
// perform the delete on a non-existent release
_, err := cfgmaps.Delete("nonexistent")
if err != ErrReleaseNotFound {
t.Fatalf("Expected ErrReleaseNotFound: got {%v}", err)
}
// perform the delete
rls, err := cfgmaps.Delete(key)
if err != nil {
t.Fatalf("Failed to delete release with key %q: %s", key, err)
}
if !reflect.DeepEqual(rel, rls) {
t.Errorf("Expected {%v}, got {%v}", rel, rls)
}
// fetch the deleted release
_, err = cfgmaps.Get(key)
if !reflect.DeepEqual(ErrReleaseNotFound, err) {
t.Errorf("Expected {%v}, got {%v}", ErrReleaseNotFound, err)
}
}

@ -17,6 +17,7 @@ limitations under the License.
package driver // import "helm.sh/helm/v3/pkg/storage/driver"
import (
"reflect"
"testing"
rspb "helm.sh/helm/v3/pkg/release"
@ -110,3 +111,130 @@ func TestRecordsRemoveAt(t *testing.T) {
t.Fatalf("Expected length of rs to be 1, got %d", len(rs))
}
}
func TestRecordsGet(t *testing.T) {
rs := records([]*record{
newRecord("rls-a.v1", releaseStub("rls-a", 1, "default", rspb.StatusSuperseded)),
newRecord("rls-a.v2", releaseStub("rls-a", 2, "default", rspb.StatusDeployed)),
})
var tests = []struct {
desc string
key string
rec *record
}{
{
"get valid key",
"rls-a.v1",
newRecord("rls-a.v1", releaseStub("rls-a", 1, "default", rspb.StatusSuperseded)),
},
{
"get invalid key",
"rls-a.v3",
nil,
},
}
for _, tt := range tests {
got := rs.Get(tt.key)
if !reflect.DeepEqual(tt.rec, got) {
t.Fatalf("Expected %v, got %v", tt.rec, got)
}
}
}
func TestRecordsIndex(t *testing.T) {
rs := records([]*record{
newRecord("rls-a.v1", releaseStub("rls-a", 1, "default", rspb.StatusSuperseded)),
newRecord("rls-a.v2", releaseStub("rls-a", 2, "default", rspb.StatusDeployed)),
})
var tests = []struct {
desc string
key string
sort int
}{
{
"get valid key",
"rls-a.v1",
0,
},
{
"get invalid key",
"rls-a.v3",
-1,
},
}
for _, tt := range tests {
got, _ := rs.Index(tt.key)
if got != tt.sort {
t.Fatalf("Expected %d, got %d", tt.sort, got)
}
}
}
func TestRecordsExists(t *testing.T) {
rs := records([]*record{
newRecord("rls-a.v1", releaseStub("rls-a", 1, "default", rspb.StatusSuperseded)),
newRecord("rls-a.v2", releaseStub("rls-a", 2, "default", rspb.StatusDeployed)),
})
var tests = []struct {
desc string
key string
ok bool
}{
{
"get valid key",
"rls-a.v1",
true,
},
{
"get invalid key",
"rls-a.v3",
false,
},
}
for _, tt := range tests {
got := rs.Exists(tt.key)
if got != tt.ok {
t.Fatalf("Expected %t, got %t", tt.ok, got)
}
}
}
func TestRecordsReplace(t *testing.T) {
rs := records([]*record{
newRecord("rls-a.v1", releaseStub("rls-a", 1, "default", rspb.StatusSuperseded)),
newRecord("rls-a.v2", releaseStub("rls-a", 2, "default", rspb.StatusDeployed)),
})
var tests = []struct {
desc string
key string
rec *record
expected *record
}{
{
"replace with existing key",
"rls-a.v2",
newRecord("rls-a.v3", releaseStub("rls-a", 3, "default", rspb.StatusSuperseded)),
newRecord("rls-a.v2", releaseStub("rls-a", 2, "default", rspb.StatusDeployed)),
},
{
"replace with non existing key",
"rls-a.v4",
newRecord("rls-a.v4", releaseStub("rls-a", 4, "default", rspb.StatusDeployed)),
nil,
},
}
for _, tt := range tests {
got := rs.Replace(tt.key, tt.rec)
if !reflect.DeepEqual(tt.expected, got) {
t.Fatalf("Expected %v, got %v", tt.expected, got)
}
}
}

@ -185,11 +185,7 @@ func (secrets *Secrets) Update(key string, rls *rspb.Release) error {
func (secrets *Secrets) Delete(key string) (rls *rspb.Release, err error) {
// fetch the release to check existence
if rls, err = secrets.Get(key); err != nil {
if apierrors.IsNotFound(err) {
return nil, ErrReleaseExists
}
return nil, errors.Wrapf(err, "delete: failed to get release %q", key)
return nil, err
}
// delete the release
err = secrets.impl.Delete(key, &metav1.DeleteOptions{})

@ -184,3 +184,34 @@ func TestSecretUpdate(t *testing.T) {
t.Errorf("Expected status %s, got status %s", rel.Info.Status.String(), got.Info.Status.String())
}
}
func TestSecretDelete(t *testing.T) {
vers := 1
name := "smug-pigeon"
namespace := "default"
key := testKey(name, vers)
rel := releaseStub(name, vers, namespace, rspb.StatusDeployed)
secrets := newTestFixtureSecrets(t, []*rspb.Release{rel}...)
// perform the delete on a non-existing release
_, err := secrets.Delete("nonexistent")
if err != ErrReleaseNotFound {
t.Fatalf("Expected ErrReleaseNotFound, got: {%v}", err)
}
// perform the delete
rls, err := secrets.Delete(key)
if err != nil {
t.Fatalf("Failed to delete release with key %q: %s", key, err)
}
if !reflect.DeepEqual(rel, rls) {
t.Errorf("Expected {%v}, got {%v}", rel, rls)
}
// fetch the deleted release
_, err = secrets.Get(key)
if !reflect.DeepEqual(ErrReleaseNotFound, err) {
t.Errorf("Expected {%v}, got {%v}", ErrReleaseNotFound, err)
}
}

@ -449,6 +449,39 @@ func TestParseIntoString(t *testing.T) {
}
}
func TestParseFile(t *testing.T) {
input := "name1=path1"
expect := map[string]interface{}{
"name1": "value1",
}
rs2v := func(rs []rune) (interface{}, error) {
v := string(rs)
if v != "path1" {
t.Errorf("%s: runesToVal: Expected value path1, got %s", input, v)
return "", nil
}
return "value1", nil
}
got, err := ParseFile(input, rs2v)
if err != nil {
t.Fatal(err)
}
y1, err := yaml.Marshal(expect)
if err != nil {
t.Fatal(err)
}
y2, err := yaml.Marshal(got)
if err != nil {
t.Fatalf("Error serializing parsed value: %s", err)
}
if string(y1) != string(y2) {
t.Errorf("%s: Expected:\n%s\nGot:\n%s", input, y1, y2)
}
}
func TestParseIntoFile(t *testing.T) {
got := map[string]interface{}{}
input := "name1=path1"

@ -78,16 +78,14 @@ verifySupported() {
# checkDesiredVersion checks if the desired version is available.
checkDesiredVersion() {
if [ "x$DESIRED_VERSION" == "x" ]; then
# FIXME(bacongobbler): hard code the desired version for the time being.
# A better fix would be to filter for Helm 2 release pages.
TAG="v2.16.1"
# Get tag from release URL
# local latest_release_url="https://github.com/helm/helm/releases/latest"
# if type "curl" > /dev/null; then
# TAG=$(curl -Ls -o /dev/null -w %{url_effective} $latest_release_url | grep -oE "[^/]+$" )
# elif type "wget" > /dev/null; then
# TAG=$(wget $latest_release_url --server-response -O /dev/null 2>&1 | awk '/^ Location: /{DEST=$2} END{ print DEST}' | grep -oE "[^/]+$")
# fi
local release_url="https://github.com/helm/helm/releases"
if type "curl" > /dev/null; then
TAG=$(curl -Ls $release_url | grep 'href="/helm/helm/releases/tag/v2.' | grep -v no-underline | head -n 1 | cut -d '"' -f 2 | awk '{n=split($NF,a,"/");print a[n]}' | awk 'a !~ $0{print}; {a=$0}')
elif type "wget" > /dev/null; then
TAG=$(wget $release_url -O - 2>&1 | grep 'href="/helm/helm/releases/tag/v2.' | grep -v no-underline | head -n 1 | cut -d '"' -f 2 | awk '{n=split($NF,a,"/");print a[n]}' | awk 'a !~ $0{print}; {a=$0}')
fi
else
TAG=$DESIRED_VERSION
fi

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save