Merge branch 'main' into feat/subcharts-scope

pull/9957/head
Matt Farina 4 years ago committed by GitHub
commit ac80a5eec9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -72,6 +72,16 @@ To load completions for every new session, execute once:
You will need to start a new shell for this setup to take effect. You will need to start a new shell for this setup to take effect.
` `
const powershellCompDesc = `
Generate the autocompletion script for powershell.
To load completions in your current shell session:
PS C:\> helm completion powershell | Out-String | Invoke-Expression
To load completions for every new session, add the output of the above command
to your powershell profile.
`
const ( const (
noDescFlagName = "no-descriptions" noDescFlagName = "no-descriptions"
noDescFlagText = "disable completion descriptions" noDescFlagText = "disable completion descriptions"
@ -88,16 +98,16 @@ func newCompletionCmd(out io.Writer) *cobra.Command {
} }
bash := &cobra.Command{ bash := &cobra.Command{
Use: "bash", Use: "bash",
Short: "generate autocompletion script for bash", Short: "generate autocompletion script for bash",
Long: bashCompDesc, Long: bashCompDesc,
Args: require.NoArgs, Args: require.NoArgs,
DisableFlagsInUseLine: true, ValidArgsFunction: noCompletions,
ValidArgsFunction: noCompletions,
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
return runCompletionBash(out, cmd) return runCompletionBash(out, cmd)
}, },
} }
bash.Flags().BoolVar(&disableCompDescriptions, noDescFlagName, false, noDescFlagText)
zsh := &cobra.Command{ zsh := &cobra.Command{
Use: "zsh", Use: "zsh",
@ -123,13 +133,25 @@ func newCompletionCmd(out io.Writer) *cobra.Command {
} }
fish.Flags().BoolVar(&disableCompDescriptions, noDescFlagName, false, noDescFlagText) fish.Flags().BoolVar(&disableCompDescriptions, noDescFlagName, false, noDescFlagText)
cmd.AddCommand(bash, zsh, fish) powershell := &cobra.Command{
Use: "powershell",
Short: "generate autocompletion script for powershell",
Long: powershellCompDesc,
Args: require.NoArgs,
ValidArgsFunction: noCompletions,
RunE: func(cmd *cobra.Command, args []string) error {
return runCompletionPowershell(out, cmd)
},
}
powershell.Flags().BoolVar(&disableCompDescriptions, noDescFlagName, false, noDescFlagText)
cmd.AddCommand(bash, zsh, fish, powershell)
return cmd return cmd
} }
func runCompletionBash(out io.Writer, cmd *cobra.Command) error { func runCompletionBash(out io.Writer, cmd *cobra.Command) error {
err := cmd.Root().GenBashCompletion(out) err := cmd.Root().GenBashCompletionV2(out, !disableCompDescriptions)
// In case the user renamed the helm binary (e.g., to be able to run // In case the user renamed the helm binary (e.g., to be able to run
// both helm2 and helm3), we hook the new binary name to the completion function // both helm2 and helm3), we hook the new binary name to the completion function
@ -180,6 +202,13 @@ func runCompletionFish(out io.Writer, cmd *cobra.Command) error {
return cmd.Root().GenFishCompletion(out, !disableCompDescriptions) return cmd.Root().GenFishCompletion(out, !disableCompDescriptions)
} }
func runCompletionPowershell(out io.Writer, cmd *cobra.Command) error {
if disableCompDescriptions {
return cmd.Root().GenPowerShellCompletion(out)
}
return cmd.Root().GenPowerShellCompletionWithDesc(out)
}
// Function to disable file completion // Function to disable file completion
func noCompletions(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { func noCompletions(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
return nil, cobra.ShellCompDirectiveNoFileComp return nil, cobra.ShellCompDirectiveNoFileComp

@ -277,11 +277,6 @@ func TestPluginDynamicCompletion(t *testing.T) {
cmd: "__complete echo -n mynamespace ''", cmd: "__complete echo -n mynamespace ''",
golden: "output/plugin_echo_no_directive.txt", golden: "output/plugin_echo_no_directive.txt",
rels: []*release.Release{}, 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 { for _, test := range tests {
settings.PluginsDirectory = "testdata/helmhome/helm/plugins" settings.PluginsDirectory = "testdata/helmhome/helm/plugins"

@ -67,7 +67,7 @@ func newRegistryLoginCmd(cfg *action.Configuration, out io.Writer) *cobra.Comman
return cmd return cmd
} }
// Adapted from https://github.com/deislabs/oras // Adapted from https://oras.land/oras-go
func getUsernamePassword(usernameOpt string, passwordOpt string, passwordFromStdinOpt bool) (string, string, error) { func getUsernamePassword(usernameOpt string, passwordOpt string, passwordFromStdinOpt bool) (string, string, error) {
var err error var err error
username := usernameOpt username := usernameOpt
@ -110,7 +110,7 @@ func getUsernamePassword(usernameOpt string, passwordOpt string, passwordFromStd
return username, password, nil return username, password, nil
} }
// Copied/adapted from https://github.com/deislabs/oras // Copied/adapted from https://oras.land/oras-go
func readLine(prompt string, silent bool) (string, error) { func readLine(prompt string, silent bool) (string, error) {
fmt.Print(prompt) fmt.Print(prompt)
if silent { if silent {

@ -48,6 +48,7 @@ type repoAddOptions struct {
url string url string
username string username string
password string password string
passwordFromStdinOpt bool
passCredentialsAll bool passCredentialsAll bool
forceUpdate bool forceUpdate bool
allowDeprecatedRepos bool allowDeprecatedRepos bool
@ -85,6 +86,7 @@ func newRepoAddCmd(out io.Writer) *cobra.Command {
f := cmd.Flags() f := cmd.Flags()
f.StringVar(&o.username, "username", "", "chart repository username") f.StringVar(&o.username, "username", "", "chart repository username")
f.StringVar(&o.password, "password", "", "chart repository password") f.StringVar(&o.password, "password", "", "chart repository password")
f.BoolVarP(&o.passwordFromStdinOpt, "password-stdin", "", false, "read chart repository password from stdin")
f.BoolVar(&o.forceUpdate, "force-update", false, "replace (overwrite) the repo if it already exists") f.BoolVar(&o.forceUpdate, "force-update", false, "replace (overwrite) the repo if it already exists")
f.BoolVar(&o.deprecatedNoUpdate, "no-update", false, "Ignored. Formerly, it would disabled forced updates. It is deprecated by force-update.") f.BoolVar(&o.deprecatedNoUpdate, "no-update", false, "Ignored. Formerly, it would disabled forced updates. It is deprecated by force-update.")
f.StringVar(&o.certFile, "cert-file", "", "identify HTTPS client using this SSL certificate file") f.StringVar(&o.certFile, "cert-file", "", "identify HTTPS client using this SSL certificate file")
@ -143,14 +145,24 @@ func (o *repoAddOptions) run(out io.Writer) error {
} }
if o.username != "" && o.password == "" { if o.username != "" && o.password == "" {
fd := int(os.Stdin.Fd()) if o.passwordFromStdinOpt {
fmt.Fprint(out, "Password: ") passwordFromStdin, err := io.ReadAll(os.Stdin)
password, err := term.ReadPassword(fd) if err != nil {
fmt.Fprintln(out) return err
if err != nil { }
return err password := strings.TrimSuffix(string(passwordFromStdin), "\n")
password = strings.TrimSuffix(password, "\r")
o.password = password
} else {
fd := int(os.Stdin.Fd())
fmt.Fprint(out, "Password: ")
password, err := term.ReadPassword(fd)
fmt.Fprintln(out)
if err != nil {
return err
}
o.password = string(password)
} }
o.password = string(password)
} }
c := repo.Entry{ c := repo.Entry{

@ -21,6 +21,7 @@ import (
"io/ioutil" "io/ioutil"
"os" "os"
"path/filepath" "path/filepath"
"strings"
"sync" "sync"
"testing" "testing"
@ -204,3 +205,33 @@ func TestRepoAddFileCompletion(t *testing.T) {
checkFileCompletion(t, "repo add reponame", false) checkFileCompletion(t, "repo add reponame", false)
checkFileCompletion(t, "repo add reponame https://example.com", false) checkFileCompletion(t, "repo add reponame https://example.com", false)
} }
func TestRepoAddWithPasswordFromStdin(t *testing.T) {
srv := repotest.NewTempServerWithCleanupAndBasicAuth(t, "testdata/testserver/*.*")
defer srv.Stop()
defer resetEnv()()
in, err := os.Open("testdata/password")
if err != nil {
t.Errorf("unexpected error, got '%v'", err)
}
tmpdir := ensure.TempDir(t)
repoFile := filepath.Join(tmpdir, "repositories.yaml")
store := storageFixture()
const testName = "test-name"
const username = "username"
cmd := fmt.Sprintf("repo add %s %s --repository-config %s --repository-cache %s --username %s --password-stdin", testName, srv.URL(), repoFile, tmpdir, username)
var result string
_, result, err = executeActionCommandStdinC(store, in, cmd)
if err != nil {
t.Errorf("unexpected error, got '%v'", err)
}
if !strings.Contains(result, fmt.Sprintf("\"%s\" has been added to your repositories", testName)) {
t.Errorf("Repo was not successfully added. Output: %s", result)
}
}

@ -17,6 +17,7 @@ limitations under the License.
package main package main
import ( import (
"fmt"
"io" "io"
"strings" "strings"
@ -131,7 +132,7 @@ func compListRepos(prefix string, ignoredRepoNames []string) []string {
filteredRepos := filterRepos(f.Repositories, ignoredRepoNames) filteredRepos := filterRepos(f.Repositories, ignoredRepoNames)
for _, repo := range filteredRepos { for _, repo := range filteredRepos {
if strings.HasPrefix(repo.Name, prefix) { if strings.HasPrefix(repo.Name, prefix) {
rNames = append(rNames, repo.Name) rNames = append(rNames, fmt.Sprintf("%s\t%s", repo.Name, repo.URL))
} }
} }
} }

@ -18,6 +18,7 @@ package main
import ( import (
"bytes" "bytes"
"fmt"
"os" "os"
"path/filepath" "path/filepath"
"strings" "strings"
@ -161,6 +162,51 @@ func testCacheFiles(t *testing.T, cacheIndexFile string, cacheChartsFile string,
} }
} }
func TestRepoRemoveCompletion(t *testing.T) {
ts, err := repotest.NewTempServerWithCleanup(t, "testdata/testserver/*.*")
if err != nil {
t.Fatal(err)
}
defer ts.Stop()
rootDir := ensure.TempDir(t)
repoFile := filepath.Join(rootDir, "repositories.yaml")
repoCache := filepath.Join(rootDir, "cache/")
var testRepoNames = []string{"foo", "bar", "baz"}
// Add test repos
for _, repoName := range testRepoNames {
o := &repoAddOptions{
name: repoName,
url: ts.URL(),
repoFile: repoFile,
}
if err := o.run(os.Stderr); err != nil {
t.Error(err)
}
}
repoSetup := fmt.Sprintf("--repository-config %s --repository-cache %s", repoFile, repoCache)
// In the following tests, we turn off descriptions for completions by using __completeNoDesc.
// We have to do this because the description will contain the port used by the webserver,
// and that port changes each time we run the test.
tests := []cmdTestCase{{
name: "completion for repo remove",
cmd: fmt.Sprintf("%s __completeNoDesc repo remove ''", repoSetup),
golden: "output/repo_list_comp.txt",
}, {
name: "completion for repo remove repetition",
cmd: fmt.Sprintf("%s __completeNoDesc repo remove foo ''", repoSetup),
golden: "output/repo_repeat_comp.txt",
}}
for _, test := range tests {
runTestCmd(t, []cmdTestCase{test})
}
}
func TestRepoRemoveFileCompletion(t *testing.T) { func TestRepoRemoveFileCompletion(t *testing.T) {
checkFileCompletion(t, "repo remove", false) checkFileCompletion(t, "repo remove", false)
checkFileCompletion(t, "repo remove repo1", false) checkFileCompletion(t, "repo remove repo1", false)

@ -41,10 +41,11 @@ To update all the repositories, use 'helm repo update'.
var errNoRepositories = errors.New("no repositories found. You must add one before updating") var errNoRepositories = errors.New("no repositories found. You must add one before updating")
type repoUpdateOptions struct { type repoUpdateOptions struct {
update func([]*repo.ChartRepository, io.Writer) update func([]*repo.ChartRepository, io.Writer, bool) error
repoFile string repoFile string
repoCache string repoCache string
names []string names []string
failOnRepoUpdateFail bool
} }
func newRepoUpdateCmd(out io.Writer) *cobra.Command { func newRepoUpdateCmd(out io.Writer) *cobra.Command {
@ -66,6 +67,13 @@ func newRepoUpdateCmd(out io.Writer) *cobra.Command {
return o.run(out) return o.run(out)
}, },
} }
f := cmd.Flags()
// Adding this flag for Helm 3 as stop gap functionality for https://github.com/helm/helm/issues/10016.
// This should be deprecated in Helm 4 by update to the behaviour of `helm repo update` command.
f.BoolVar(&o.failOnRepoUpdateFail, "fail-on-repo-update-fail", false, "update fails if any of the repository updates fail")
return cmd return cmd
} }
@ -103,26 +111,34 @@ func (o *repoUpdateOptions) run(out io.Writer) error {
} }
} }
o.update(repos, out) return o.update(repos, out, o.failOnRepoUpdateFail)
return nil
} }
func updateCharts(repos []*repo.ChartRepository, out io.Writer) { func updateCharts(repos []*repo.ChartRepository, out io.Writer, failOnRepoUpdateFail bool) error {
fmt.Fprintln(out, "Hang tight while we grab the latest from your chart repositories...") fmt.Fprintln(out, "Hang tight while we grab the latest from your chart repositories...")
var wg sync.WaitGroup var wg sync.WaitGroup
var repoFailList []string
for _, re := range repos { for _, re := range repos {
wg.Add(1) wg.Add(1)
go func(re *repo.ChartRepository) { go func(re *repo.ChartRepository) {
defer wg.Done() defer wg.Done()
if _, err := re.DownloadIndexFile(); err != nil { if _, err := re.DownloadIndexFile(); err != nil {
fmt.Fprintf(out, "...Unable to get an update from the %q chart repository (%s):\n\t%s\n", re.Config.Name, re.Config.URL, err) fmt.Fprintf(out, "...Unable to get an update from the %q chart repository (%s):\n\t%s\n", re.Config.Name, re.Config.URL, err)
repoFailList = append(repoFailList, re.Config.URL)
} else { } else {
fmt.Fprintf(out, "...Successfully got an update from the %q chart repository\n", re.Config.Name) fmt.Fprintf(out, "...Successfully got an update from the %q chart repository\n", re.Config.Name)
} }
}(re) }(re)
} }
wg.Wait() wg.Wait()
if len(repoFailList) > 0 && failOnRepoUpdateFail {
return errors.New(fmt.Sprintf("Failed to update the following repositories: %s",
repoFailList))
}
fmt.Fprintln(out, "Update Complete. ⎈Happy Helming!⎈") fmt.Fprintln(out, "Update Complete. ⎈Happy Helming!⎈")
return nil
} }
func checkRequestedRepos(requestedRepos []string, validRepos []*repo.Entry) error { func checkRequestedRepos(requestedRepos []string, validRepos []*repo.Entry) error {

@ -35,10 +35,11 @@ func TestUpdateCmd(t *testing.T) {
var out bytes.Buffer var out bytes.Buffer
// Instead of using the HTTP updater, we provide our own for this test. // Instead of using the HTTP updater, we provide our own for this test.
// The TestUpdateCharts test verifies the HTTP behavior independently. // The TestUpdateCharts test verifies the HTTP behavior independently.
updater := func(repos []*repo.ChartRepository, out io.Writer) { updater := func(repos []*repo.ChartRepository, out io.Writer, failOnRepoUpdateFail bool) error {
for _, re := range repos { for _, re := range repos {
fmt.Fprintln(out, re.Config.Name) fmt.Fprintln(out, re.Config.Name)
} }
return nil
} }
o := &repoUpdateOptions{ o := &repoUpdateOptions{
update: updater, update: updater,
@ -59,10 +60,11 @@ func TestUpdateCmdMultiple(t *testing.T) {
var out bytes.Buffer var out bytes.Buffer
// Instead of using the HTTP updater, we provide our own for this test. // Instead of using the HTTP updater, we provide our own for this test.
// The TestUpdateCharts test verifies the HTTP behavior independently. // The TestUpdateCharts test verifies the HTTP behavior independently.
updater := func(repos []*repo.ChartRepository, out io.Writer) { updater := func(repos []*repo.ChartRepository, out io.Writer, failOnRepoUpdateFail bool) error {
for _, re := range repos { for _, re := range repos {
fmt.Fprintln(out, re.Config.Name) fmt.Fprintln(out, re.Config.Name)
} }
return nil
} }
o := &repoUpdateOptions{ o := &repoUpdateOptions{
update: updater, update: updater,
@ -84,10 +86,11 @@ func TestUpdateCmdInvalid(t *testing.T) {
var out bytes.Buffer var out bytes.Buffer
// Instead of using the HTTP updater, we provide our own for this test. // Instead of using the HTTP updater, we provide our own for this test.
// The TestUpdateCharts test verifies the HTTP behavior independently. // The TestUpdateCharts test verifies the HTTP behavior independently.
updater := func(repos []*repo.ChartRepository, out io.Writer) { updater := func(repos []*repo.ChartRepository, out io.Writer, failOnRepoUpdateFail bool) error {
for _, re := range repos { for _, re := range repos {
fmt.Fprintln(out, re.Config.Name) fmt.Fprintln(out, re.Config.Name)
} }
return nil
} }
o := &repoUpdateOptions{ o := &repoUpdateOptions{
update: updater, update: updater,
@ -144,7 +147,7 @@ func TestUpdateCharts(t *testing.T) {
} }
b := bytes.NewBuffer(nil) b := bytes.NewBuffer(nil)
updateCharts([]*repo.ChartRepository{r}, b) updateCharts([]*repo.ChartRepository{r}, b, false)
got := b.String() got := b.String()
if strings.Contains(got, "Unable to get an update") { if strings.Contains(got, "Unable to get an update") {
@ -159,3 +162,79 @@ func TestRepoUpdateFileCompletion(t *testing.T) {
checkFileCompletion(t, "repo update", false) checkFileCompletion(t, "repo update", false)
checkFileCompletion(t, "repo update repo1", false) checkFileCompletion(t, "repo update repo1", false)
} }
func TestUpdateChartsFail(t *testing.T) {
defer resetEnv()()
defer ensure.HelmHome(t)()
ts, err := repotest.NewTempServerWithCleanup(t, "testdata/testserver/*.*")
if err != nil {
t.Fatal(err)
}
defer ts.Stop()
var invalidURL = ts.URL() + "55"
r, err := repo.NewChartRepository(&repo.Entry{
Name: "charts",
URL: invalidURL,
}, getter.All(settings))
if err != nil {
t.Error(err)
}
b := bytes.NewBuffer(nil)
if err := updateCharts([]*repo.ChartRepository{r}, b, false); err != nil {
t.Error("Repo update should not return error if update of repository fails")
}
got := b.String()
if !strings.Contains(got, "Unable to get an update") {
t.Errorf("Repo should have failed update but instead got: %q", got)
}
if !strings.Contains(got, "Update Complete.") {
t.Error("Update was not successful")
}
}
func TestUpdateChartsFailWithError(t *testing.T) {
defer resetEnv()()
defer ensure.HelmHome(t)()
ts, err := repotest.NewTempServerWithCleanup(t, "testdata/testserver/*.*")
if err != nil {
t.Fatal(err)
}
defer ts.Stop()
var invalidURL = ts.URL() + "55"
r, err := repo.NewChartRepository(&repo.Entry{
Name: "charts",
URL: invalidURL,
}, getter.All(settings))
if err != nil {
t.Error(err)
}
b := bytes.NewBuffer(nil)
err = updateCharts([]*repo.ChartRepository{r}, b, true)
if err == nil {
t.Error("Repo update should return error because update of repository fails and 'fail-on-repo-update-fail' flag set")
return
}
var expectedErr = "Failed to update the following repositories"
var receivedErr = err.Error()
if !strings.Contains(receivedErr, expectedErr) {
t.Errorf("Expected error (%s) but got (%s) instead", expectedErr, receivedErr)
}
if !strings.Contains(receivedErr, invalidURL) {
t.Errorf("Expected invalid URL (%s) in error message but got (%s) instead", invalidURL, receivedErr)
}
got := b.String()
if !strings.Contains(got, "Unable to get an update") {
t.Errorf("Repo should have failed update but instead got: %q", got)
}
if strings.Contains(got, "Update Complete.") {
t.Error("Update was not successful and should return error message because 'fail-on-repo-update-fail' flag set")
}
}

@ -302,7 +302,14 @@ func compListCharts(toComplete string, includeFiles bool) ([]string, cobra.Shell
// First check completions for repos // First check completions for repos
repos := compListRepos("", nil) repos := compListRepos("", nil)
for _, repo := range repos { for _, repoInfo := range repos {
// Split name from description
repoInfo := strings.Split(repoInfo, "\t")
repo := repoInfo[0]
repoDesc := ""
if len(repoInfo) > 1 {
repoDesc = repoInfo[1]
}
repoWithSlash := fmt.Sprintf("%s/", repo) repoWithSlash := fmt.Sprintf("%s/", repo)
if strings.HasPrefix(toComplete, repoWithSlash) { if strings.HasPrefix(toComplete, repoWithSlash) {
// Must complete with charts within the specified repo // Must complete with charts within the specified repo
@ -310,15 +317,15 @@ func compListCharts(toComplete string, includeFiles bool) ([]string, cobra.Shell
noSpace = false noSpace = false
break break
} else if strings.HasPrefix(repo, toComplete) { } else if strings.HasPrefix(repo, toComplete) {
// Must complete the repo name // Must complete the repo name with the slash, followed by the description
completions = append(completions, repoWithSlash) completions = append(completions, fmt.Sprintf("%s\t%s", repoWithSlash, repoDesc))
noSpace = true noSpace = true
} }
} }
cobra.CompDebugln(fmt.Sprintf("Completions after repos: %v", completions), settings.Debug) cobra.CompDebugln(fmt.Sprintf("Completions after repos: %v", completions), settings.Debug)
// Now handle completions for url prefixes // Now handle completions for url prefixes
for _, url := range []string{"https://", "http://", "file://"} { for _, url := range []string{"https://\tChart URL prefix", "http://\tChart URL prefix", "file://\tChart local URL prefix"} {
if strings.HasPrefix(toComplete, url) { if strings.HasPrefix(toComplete, url) {
// The user already put in the full url prefix; we don't have // The user already put in the full url prefix; we don't have
// anything to add, but make sure the shell does not default // anything to add, but make sure the shell does not default
@ -355,7 +362,7 @@ func compListCharts(toComplete string, includeFiles bool) ([]string, cobra.Shell
// If the user didn't provide any input to completion, // If the user didn't provide any input to completion,
// we provide a hint that a path can also be used // we provide a hint that a path can also be used
if includeFiles && len(toComplete) == 0 { if includeFiles && len(toComplete) == 0 {
completions = append(completions, "./", "/") completions = append(completions, "./\tRelative path prefix to local chart", "/\tAbsolute path prefix to local chart")
} }
cobra.CompDebugln(fmt.Sprintf("Completions after checking empty input: %v", completions), settings.Debug) cobra.CompDebugln(fmt.Sprintf("Completions after checking empty input: %v", completions), settings.Debug)

@ -51,6 +51,11 @@ This command inspects a chart (directory, file, or URL) and displays the content
of the README file of the README file
` `
const showCRDsDesc = `
This command inspects a chart (directory, file, or URL) and displays the contents
of the CustomResourceDefintion files
`
func newShowCmd(out io.Writer) *cobra.Command { func newShowCmd(out io.Writer) *cobra.Command {
client := action.NewShow(action.ShowAll) client := action.NewShow(action.ShowAll)
@ -139,7 +144,24 @@ func newShowCmd(out io.Writer) *cobra.Command {
}, },
} }
cmds := []*cobra.Command{all, readmeSubCmd, valuesSubCmd, chartSubCmd} crdsSubCmd := &cobra.Command{
Use: "crds [CHART]",
Short: "show the chart's CRDs",
Long: showCRDsDesc,
Args: require.ExactArgs(1),
ValidArgsFunction: validArgsFunc,
RunE: func(cmd *cobra.Command, args []string) error {
client.OutputFormat = action.ShowCRDs
output, err := runShow(args, client)
if err != nil {
return err
}
fmt.Fprint(out, output)
return nil
},
}
cmds := []*cobra.Command{all, readmeSubCmd, valuesSubCmd, chartSubCmd, crdsSubCmd}
for _, subCmd := range cmds { for _, subCmd := range cmds {
addShowFlags(subCmd, client) addShowFlags(subCmd, client)
showCommand.AddCommand(subCmd) showCommand.AddCommand(subCmd)

@ -145,3 +145,7 @@ func TestShowReadmeFileCompletion(t *testing.T) {
func TestShowValuesFileCompletion(t *testing.T) { func TestShowValuesFileCompletion(t *testing.T) {
checkFileCompletion(t, "show values", true) checkFileCompletion(t, "show values", true)
} }
func TestShowCRDsFileCompletion(t *testing.T) {
checkFileCompletion(t, "show crds", true)
}

@ -7,8 +7,7 @@ echo "Args received: ${@}"
# Final printout is the optional completion directive of the form :<directive> # Final printout is the optional completion directive of the form :<directive>
if [ "$HELM_NAMESPACE" = "default" ]; then if [ "$HELM_NAMESPACE" = "default" ]; then
# Output an invalid directive, which should be ignored echo ":0"
echo ":2222"
# else # else
# Don't include the directive, to test it is really optional # Don't include the directive, to test it is really optional
fi fi

@ -1,6 +0,0 @@
echo plugin.complete was called
Namespace: default
Num args received: 1
Args received:
:0
Completion ended with directive: ShellCompDirectiveDefault

@ -0,0 +1,5 @@
foo
bar
baz
:4
Completion ended with directive: ShellCompDirectiveNoFileComp

@ -0,0 +1,4 @@
bar
baz
:4
Completion ended with directive: ShellCompDirectiveNoFileComp

@ -0,0 +1 @@
password

@ -10,10 +10,9 @@ require (
github.com/Masterminds/squirrel v1.5.0 github.com/Masterminds/squirrel v1.5.0
github.com/Masterminds/vcs v1.13.1 github.com/Masterminds/vcs v1.13.1
github.com/asaskevich/govalidator v0.0.0-20200428143746-21a406dcc535 github.com/asaskevich/govalidator v0.0.0-20200428143746-21a406dcc535
github.com/containerd/containerd v1.4.4 github.com/containerd/containerd v1.5.4
github.com/cyphar/filepath-securejoin v0.2.2 github.com/cyphar/filepath-securejoin v0.2.2
github.com/deislabs/oras v0.11.1 github.com/distribution/distribution/v3 v3.0.0-20210804104954-38ab4c606ee3
github.com/docker/distribution v2.7.1+incompatible
github.com/docker/docker v17.12.0-ce-rc1.0.20200618181300-9dc6525e6118+incompatible github.com/docker/docker v17.12.0-ce-rc1.0.20200618181300-9dc6525e6118+incompatible
github.com/docker/go-units v0.4.0 github.com/docker/go-units v0.4.0
github.com/evanphx/json-patch v4.9.0+incompatible github.com/evanphx/json-patch v4.9.0+incompatible
@ -28,22 +27,24 @@ require (
github.com/opencontainers/image-spec v1.0.1 github.com/opencontainers/image-spec v1.0.1
github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2 github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2
github.com/pkg/errors v0.9.1 github.com/pkg/errors v0.9.1
github.com/rubenv/sql-migrate v0.0.0-20200616145509-8d140a17f351 github.com/rubenv/sql-migrate v0.0.0-20210614095031-55d5740dbbcc
github.com/sirupsen/logrus v1.8.1 github.com/sirupsen/logrus v1.8.1
github.com/spf13/cobra v1.1.3 github.com/spf13/cobra v1.2.1
github.com/spf13/pflag v1.0.5 github.com/spf13/pflag v1.0.5
github.com/stretchr/testify v1.7.0 github.com/stretchr/testify v1.7.0
github.com/xeipuuv/gojsonschema v1.2.0 github.com/xeipuuv/gojsonschema v1.2.0
golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83 github.com/ziutek/mymysql v1.5.4 // indirect
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a
golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d
k8s.io/api v0.21.0 k8s.io/api v0.21.3
k8s.io/apiextensions-apiserver v0.21.0 k8s.io/apiextensions-apiserver v0.21.3
k8s.io/apimachinery v0.21.0 k8s.io/apimachinery v0.21.3
k8s.io/apiserver v0.21.0 k8s.io/apiserver v0.21.3
k8s.io/cli-runtime v0.21.0 k8s.io/cli-runtime v0.21.3
k8s.io/client-go v0.21.0 k8s.io/client-go v0.21.3
k8s.io/klog/v2 v2.8.0 k8s.io/klog/v2 v2.8.0
k8s.io/kubectl v0.21.0 k8s.io/kubectl v0.21.3
oras.land/oras-go v0.4.0
sigs.k8s.io/yaml v1.2.0 sigs.k8s.io/yaml v1.2.0
) )

770
go.sum

File diff suppressed because it is too large Load Diff

@ -17,7 +17,7 @@ limitations under the License.
package registry // import "helm.sh/helm/v3/internal/experimental/registry" package registry // import "helm.sh/helm/v3/internal/experimental/registry"
import ( import (
"github.com/deislabs/oras/pkg/auth" "oras.land/oras-go/pkg/auth"
) )
type ( type (

@ -28,11 +28,11 @@ import (
"github.com/containerd/containerd/content" "github.com/containerd/containerd/content"
"github.com/containerd/containerd/errdefs" "github.com/containerd/containerd/errdefs"
orascontent "github.com/deislabs/oras/pkg/content"
digest "github.com/opencontainers/go-digest" digest "github.com/opencontainers/go-digest"
specs "github.com/opencontainers/image-spec/specs-go" specs "github.com/opencontainers/image-spec/specs-go"
ocispec "github.com/opencontainers/image-spec/specs-go/v1" ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/pkg/errors" "github.com/pkg/errors"
orascontent "oras.land/oras-go/pkg/content"
"helm.sh/helm/v3/pkg/chart" "helm.sh/helm/v3/pkg/chart"
"helm.sh/helm/v3/pkg/chart/loader" "helm.sh/helm/v3/pkg/chart/loader"

@ -25,12 +25,12 @@ import (
"net/http" "net/http"
"sort" "sort"
auth "github.com/deislabs/oras/pkg/auth/docker"
"github.com/deislabs/oras/pkg/content"
"github.com/deislabs/oras/pkg/oras"
"github.com/gosuri/uitable" "github.com/gosuri/uitable"
ocispec "github.com/opencontainers/image-spec/specs-go/v1" ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/pkg/errors" "github.com/pkg/errors"
auth "oras.land/oras-go/pkg/auth/docker"
"oras.land/oras-go/pkg/content"
"oras.land/oras-go/pkg/oras"
"helm.sh/helm/v3/pkg/chart" "helm.sh/helm/v3/pkg/chart"
"helm.sh/helm/v3/pkg/helmpath" "helm.sh/helm/v3/pkg/helmpath"

@ -32,14 +32,14 @@ import (
"time" "time"
"github.com/containerd/containerd/errdefs" "github.com/containerd/containerd/errdefs"
auth "github.com/deislabs/oras/pkg/auth/docker" "github.com/distribution/distribution/v3/configuration"
"github.com/docker/distribution/configuration" "github.com/distribution/distribution/v3/registry"
"github.com/docker/distribution/registry" _ "github.com/distribution/distribution/v3/registry/auth/htpasswd"
_ "github.com/docker/distribution/registry/auth/htpasswd" _ "github.com/distribution/distribution/v3/registry/storage/driver/inmemory"
_ "github.com/docker/distribution/registry/storage/driver/inmemory"
"github.com/phayes/freeport" "github.com/phayes/freeport"
"github.com/stretchr/testify/suite" "github.com/stretchr/testify/suite"
"golang.org/x/crypto/bcrypt" "golang.org/x/crypto/bcrypt"
auth "oras.land/oras-go/pkg/auth/docker"
"helm.sh/helm/v3/pkg/chart" "helm.sh/helm/v3/pkg/chart"
) )

@ -22,9 +22,9 @@ import (
"io" "io"
"time" "time"
orascontext "github.com/deislabs/oras/pkg/context"
units "github.com/docker/go-units" units "github.com/docker/go-units"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
orascontext "oras.land/oras-go/pkg/context"
) )
// byteCountBinary produces a human-readable file size // byteCountBinary produces a human-readable file size

@ -16,6 +16,7 @@ limitations under the License.
package resolver package resolver
import ( import (
"runtime"
"testing" "testing"
"helm.sh/helm/v3/pkg/chart" "helm.sh/helm/v3/pkg/chart"
@ -246,24 +247,28 @@ func TestGetLocalPath(t *testing.T) {
repo string repo string
chartpath string chartpath string
expect string expect string
winExpect string
err bool err bool
}{ }{
{ {
name: "absolute path", name: "absolute path",
repo: "file:////tmp", repo: "file:////",
expect: "/tmp", expect: "/",
winExpect: "\\",
}, },
{ {
name: "relative path", name: "relative path",
repo: "file://../../testdata/chartpath/base", repo: "file://../../testdata/chartpath/base",
chartpath: "foo/bar", chartpath: "foo/bar",
expect: "testdata/chartpath/base", expect: "testdata/chartpath/base",
winExpect: "testdata\\chartpath\\base",
}, },
{ {
name: "current directory path", name: "current directory path",
repo: "../charts/localdependency", repo: "../charts/localdependency",
chartpath: "testdata/chartpath/charts", chartpath: "testdata/chartpath/charts",
expect: "testdata/chartpath/charts/localdependency", expect: "testdata/chartpath/charts/localdependency",
winExpect: "testdata\\chartpath\\charts\\localdependency",
}, },
{ {
name: "invalid local path", name: "invalid local path",
@ -291,8 +296,12 @@ func TestGetLocalPath(t *testing.T) {
if tt.err { if tt.err {
t.Fatalf("Expected error in test %q", tt.name) t.Fatalf("Expected error in test %q", tt.name)
} }
if p != tt.expect { expect := tt.expect
t.Errorf("%q: expected %q, got %q", tt.name, tt.expect, p) if runtime.GOOS == "windows" {
expect = tt.winExpect
}
if p != expect {
t.Errorf("%q: expected %q, got %q", tt.name, expect, p)
} }
}) })
} }

@ -24,8 +24,8 @@ import (
"path/filepath" "path/filepath"
"testing" "testing"
dockerauth "github.com/deislabs/oras/pkg/auth/docker"
fakeclientset "k8s.io/client-go/kubernetes/fake" fakeclientset "k8s.io/client-go/kubernetes/fake"
dockerauth "oras.land/oras-go/pkg/auth/docker"
"helm.sh/helm/v3/internal/experimental/registry" "helm.sh/helm/v3/internal/experimental/registry"
"helm.sh/helm/v3/pkg/chart" "helm.sh/helm/v3/pkg/chart"

@ -17,6 +17,7 @@ limitations under the License.
package action package action
import ( import (
"bytes"
"fmt" "fmt"
"strings" "strings"
@ -41,6 +42,8 @@ const (
ShowValues ShowOutputFormat = "values" ShowValues ShowOutputFormat = "values"
// ShowReadme is the format which only shows the chart's README // ShowReadme is the format which only shows the chart's README
ShowReadme ShowOutputFormat = "readme" ShowReadme ShowOutputFormat = "readme"
// ShowCRDs is the format which only shows the chart's CRDs
ShowCRDs ShowOutputFormat = "crds"
) )
var readmeFileNames = []string{"readme.md", "readme.txt", "readme"} var readmeFileNames = []string{"readme.md", "readme.txt", "readme"}
@ -115,6 +118,18 @@ func (s *Show) Run(chartpath string) (string, error) {
} }
fmt.Fprintf(&out, "%s\n", readme.Data) fmt.Fprintf(&out, "%s\n", readme.Data)
} }
if s.OutputFormat == ShowCRDs || s.OutputFormat == ShowAll {
crds := s.chart.CRDObjects()
if len(crds) > 0 {
if s.OutputFormat == ShowAll && !bytes.HasPrefix(crds[0].File.Data, []byte("---")) {
fmt.Fprintln(&out, "---")
}
for _, crd := range crds {
fmt.Fprintf(&out, "%s\n", string(crd.File.Data))
}
}
}
return out.String(), nil return out.String(), nil
} }

@ -28,6 +28,9 @@ func TestShow(t *testing.T) {
Metadata: &chart.Metadata{Name: "alpine"}, Metadata: &chart.Metadata{Name: "alpine"},
Files: []*chart.File{ Files: []*chart.File{
{Name: "README.md", Data: []byte("README\n")}, {Name: "README.md", Data: []byte("README\n")},
{Name: "crds/ignoreme.txt", Data: []byte("error")},
{Name: "crds/foo.yaml", Data: []byte("---\nfoo\n")},
{Name: "crds/bar.json", Data: []byte("---\nbar\n")},
}, },
Raw: []*chart.File{ Raw: []*chart.File{
{Name: "values.yaml", Data: []byte("VALUES\n")}, {Name: "values.yaml", Data: []byte("VALUES\n")},
@ -48,6 +51,12 @@ VALUES
--- ---
README README
---
foo
---
bar
` `
if output != expect { if output != expect {
t.Errorf("Expected\n%q\nGot\n%q\n", expect, output) t.Errorf("Expected\n%q\nGot\n%q\n", expect, output)
@ -83,3 +92,31 @@ func TestShowValuesByJsonPathFormat(t *testing.T) {
t.Errorf("Expected\n%q\nGot\n%q\n", expect, output) t.Errorf("Expected\n%q\nGot\n%q\n", expect, output)
} }
} }
func TestShowCRDs(t *testing.T) {
client := NewShow(ShowCRDs)
client.chart = &chart.Chart{
Metadata: &chart.Metadata{Name: "alpine"},
Files: []*chart.File{
{Name: "crds/ignoreme.txt", Data: []byte("error")},
{Name: "crds/foo.yaml", Data: []byte("---\nfoo\n")},
{Name: "crds/bar.json", Data: []byte("---\nbar\n")},
},
}
output, err := client.Run("")
if err != nil {
t.Fatal(err)
}
expect := `---
foo
---
bar
`
if output != expect {
t.Errorf("Expected\n%q\nGot\n%q\n", expect, output)
}
}

@ -16,7 +16,6 @@ limitations under the License.
package downloader package downloader
import ( import (
"net/http"
"os" "os"
"path/filepath" "path/filepath"
"testing" "testing"
@ -171,19 +170,7 @@ func TestIsTar(t *testing.T) {
} }
func TestDownloadTo(t *testing.T) { func TestDownloadTo(t *testing.T) {
// Set up a fake repo with basic auth enabled srv := repotest.NewTempServerWithCleanupAndBasicAuth(t, "testdata/*.tgz*")
srv, err := repotest.NewTempServerWithCleanup(t, "testdata/*.tgz*")
srv.Stop()
if err != nil {
t.Fatal(err)
}
srv.WithMiddleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
username, password, ok := r.BasicAuth()
if !ok || username != "username" || password != "password" {
t.Errorf("Expected request to use basic auth and for username == 'username' and password == 'password', got '%v', '%s', '%s'", ok, username, password)
}
}))
srv.Start()
defer srv.Stop() defer srv.Stop()
if err := srv.CreateIndex(); err != nil { if err := srv.CreateIndex(); err != nil {
t.Fatal(err) t.Fatal(err)

@ -249,22 +249,24 @@ func (m *Manager) downloadAll(deps []*chart.Dependency) error {
destPath := filepath.Join(m.ChartPath, "charts") destPath := filepath.Join(m.ChartPath, "charts")
tmpPath := filepath.Join(m.ChartPath, "tmpcharts") tmpPath := filepath.Join(m.ChartPath, "tmpcharts")
// Create 'charts' directory if it doesn't already exist. // Check if 'charts' directory is not actally a directory. If it does not exist, create it.
if fi, err := os.Stat(destPath); err != nil { if fi, err := os.Stat(destPath); err == nil {
if !fi.IsDir() {
return errors.Errorf("%q is not a directory", destPath)
}
} else if os.IsNotExist(err) {
if err := os.MkdirAll(destPath, 0755); err != nil { if err := os.MkdirAll(destPath, 0755); err != nil {
return err return err
} }
} else if !fi.IsDir() { } else {
return errors.Errorf("%q is not a directory", destPath) return fmt.Errorf("unable to retrieve file info for '%s': %v", destPath, err)
}
if err := fs.RenameWithFallback(destPath, tmpPath); err != nil {
return errors.Wrap(err, "unable to move current charts to tmp dir")
} }
if err := os.MkdirAll(destPath, 0755); err != nil { // Prepare tmpPath
if err := os.MkdirAll(tmpPath, 0755); err != nil {
return err return err
} }
defer os.RemoveAll(tmpPath)
fmt.Fprintf(m.Out, "Saving %d charts\n", len(deps)) fmt.Fprintf(m.Out, "Saving %d charts\n", len(deps))
var saveError error var saveError error
@ -273,10 +275,11 @@ func (m *Manager) downloadAll(deps []*chart.Dependency) error {
// No repository means the chart is in charts directory // No repository means the chart is in charts directory
if dep.Repository == "" { if dep.Repository == "" {
fmt.Fprintf(m.Out, "Dependency %s did not declare a repository. Assuming it exists in the charts directory\n", dep.Name) fmt.Fprintf(m.Out, "Dependency %s did not declare a repository. Assuming it exists in the charts directory\n", dep.Name)
chartPath := filepath.Join(tmpPath, dep.Name) // NOTE: we are only validating the local dependency conforms to the constraints. No copying to tmpPath is necessary.
chartPath := filepath.Join(destPath, dep.Name)
ch, err := loader.LoadDir(chartPath) ch, err := loader.LoadDir(chartPath)
if err != nil { if err != nil {
return fmt.Errorf("unable to load chart: %v", err) return fmt.Errorf("unable to load chart '%s': %v", chartPath, err)
} }
constraint, err := semver.NewConstraint(dep.Version) constraint, err := semver.NewConstraint(dep.Version)
@ -354,8 +357,7 @@ func (m *Manager) downloadAll(deps []*chart.Dependency) error {
getter.WithTagName(version)) getter.WithTagName(version))
} }
_, _, err = dl.DownloadTo(churl, version, destPath) if _, _, err = dl.DownloadTo(churl, version, tmpPath); err != nil {
if err != nil {
saveError = errors.Wrapf(err, "could not download %s", churl) saveError = errors.Wrapf(err, "could not download %s", churl)
break break
} }
@ -363,36 +365,14 @@ func (m *Manager) downloadAll(deps []*chart.Dependency) error {
churls[churl] = struct{}{} churls[churl] = struct{}{}
} }
// TODO: this should probably be refactored to be a []error, so we can capture and provide more information rather than "last error wins".
if saveError == nil { if saveError == nil {
fmt.Fprintln(m.Out, "Deleting outdated charts") // now we can move all downloaded charts to destPath and delete outdated dependencies
for _, dep := range deps { if err := m.safeMoveDeps(deps, tmpPath, destPath); err != nil {
// Chart from local charts directory stays in place
if dep.Repository != "" {
if err := m.safeDeleteDep(dep.Name, tmpPath); err != nil {
return err
}
}
}
if err := move(tmpPath, destPath); err != nil {
return err return err
} }
if err := os.RemoveAll(tmpPath); err != nil {
return errors.Wrapf(err, "failed to remove %v", tmpPath)
}
} else { } else {
fmt.Fprintln(m.Out, "Save error occurred: ", saveError) fmt.Fprintln(m.Out, "Save error occurred: ", saveError)
fmt.Fprintln(m.Out, "Deleting newly downloaded charts, restoring pre-update state")
for _, dep := range deps {
if err := m.safeDeleteDep(dep.Name, destPath); err != nil {
return err
}
}
if err := os.RemoveAll(destPath); err != nil {
return errors.Wrapf(err, "failed to remove %v", destPath)
}
if err := fs.RenameWithFallback(tmpPath, destPath); err != nil {
return errors.Wrap(err, "unable to move current charts to tmp dir")
}
return saveError return saveError
} }
return nil return nil
@ -410,36 +390,75 @@ func parseOCIRef(chartRef string) (string, string, error) {
return chartRef, tag, nil return chartRef, tag, nil
} }
// safeDeleteDep deletes any versions of the given dependency in the given directory. // safeMoveDep moves all dependencies in the source and moves them into dest.
// //
// It does this by first matching the file name to an expected pattern, then loading // It does this by first matching the file name to an expected pattern, then loading
// the file to verify that it is a chart with the same name as the given name. // the file to verify that it is a chart.
// //
// Because it requires tar file introspection, it is more intensive than a basic delete. // Any charts in dest that do not exist in source are removed (barring local dependencies)
//
// Because it requires tar file introspection, it is more intensive than a basic move.
// //
// This will only return errors that should stop processing entirely. Other errors // This will only return errors that should stop processing entirely. Other errors
// will emit log messages or be ignored. // will emit log messages or be ignored.
func (m *Manager) safeDeleteDep(name, dir string) error { func (m *Manager) safeMoveDeps(deps []*chart.Dependency, source, dest string) error {
files, err := filepath.Glob(filepath.Join(dir, name+"-*.tgz")) existsInSourceDirectory := map[string]bool{}
isLocalDependency := map[string]bool{}
sourceFiles, err := ioutil.ReadDir(source)
if err != nil { if err != nil {
// Only for ErrBadPattern
return err return err
} }
for _, fname := range files { // attempt to read destFiles; fail fast if we can't
ch, err := loader.LoadFile(fname) destFiles, err := ioutil.ReadDir(dest)
if err != nil { if err != nil {
fmt.Fprintf(m.Out, "Could not verify %s for deletion: %s (Skipping)", fname, err) return err
}
for _, dep := range deps {
if dep.Repository == "" {
isLocalDependency[dep.Name] = true
}
}
for _, file := range sourceFiles {
if file.IsDir() {
continue continue
} }
if ch.Name() != name { filename := file.Name()
// This is not the file you are looking for. sourcefile := filepath.Join(source, filename)
destfile := filepath.Join(dest, filename)
existsInSourceDirectory[filename] = true
if _, err := loader.LoadFile(sourcefile); err != nil {
fmt.Fprintf(m.Out, "Could not verify %s for moving: %s (Skipping)", sourcefile, err)
continue continue
} }
if err := os.Remove(fname); err != nil { // NOTE: no need to delete the dest; os.Rename replaces it.
fmt.Fprintf(m.Out, "Could not delete %s: %s (Skipping)", fname, err) if err := fs.RenameWithFallback(sourcefile, destfile); err != nil {
fmt.Fprintf(m.Out, "Unable to move %s to charts dir %s (Skipping)", sourcefile, err)
continue continue
} }
} }
fmt.Fprintln(m.Out, "Deleting outdated charts")
// find all files that exist in dest that do not exist in source; delete them (outdated dependendencies)
for _, file := range destFiles {
if !file.IsDir() && !existsInSourceDirectory[file.Name()] {
fname := filepath.Join(dest, file.Name())
ch, err := loader.LoadFile(fname)
if err != nil {
fmt.Fprintf(m.Out, "Could not verify %s for deletion: %s (Skipping)", fname, err)
}
// local dependency - skip
if isLocalDependency[ch.Name()] {
continue
}
if err := os.Remove(fname); err != nil {
fmt.Fprintf(m.Out, "Could not delete %s: %s (Skipping)", fname, err)
continue
}
}
}
return nil return nil
} }
@ -868,20 +887,6 @@ func tarFromLocalDir(chartpath, name, repo, version string) (string, error) {
return "", errors.Errorf("can't get a valid version for dependency %s", name) return "", errors.Errorf("can't get a valid version for dependency %s", name)
} }
// move files from tmppath to destpath
func move(tmpPath, destPath string) error {
files, _ := os.ReadDir(tmpPath)
for _, file := range files {
filename := file.Name()
tmpfile := filepath.Join(tmpPath, filename)
destfile := filepath.Join(destPath, filename)
if err := fs.RenameWithFallback(tmpfile, destfile); err != nil {
return errors.Wrap(err, "unable to move local charts to charts dir")
}
}
return nil
}
// The prefix to use for cache keys created by the manager for repo names // The prefix to use for cache keys created by the manager for repo names
const managerKeyPrefix = "helm-manager-" const managerKeyPrefix = "helm-manager-"

@ -17,11 +17,14 @@ package downloader
import ( import (
"bytes" "bytes"
"io/ioutil"
"os"
"path/filepath" "path/filepath"
"reflect" "reflect"
"testing" "testing"
"helm.sh/helm/v3/pkg/chart" "helm.sh/helm/v3/pkg/chart"
"helm.sh/helm/v3/pkg/chart/loader"
"helm.sh/helm/v3/pkg/chartutil" "helm.sh/helm/v3/pkg/chartutil"
"helm.sh/helm/v3/pkg/getter" "helm.sh/helm/v3/pkg/getter"
"helm.sh/helm/v3/pkg/repo/repotest" "helm.sh/helm/v3/pkg/repo/repotest"
@ -213,6 +216,54 @@ func TestGetRepoNames(t *testing.T) {
} }
} }
func TestDownloadAll(t *testing.T) {
chartPath, err := ioutil.TempDir("", "test-downloadall")
if err != nil {
t.Fatalf("could not create tempdir: %v", err)
}
defer os.RemoveAll(chartPath)
m := &Manager{
Out: new(bytes.Buffer),
RepositoryConfig: repoConfig,
RepositoryCache: repoCache,
ChartPath: chartPath,
}
signtest, err := loader.LoadDir(filepath.Join("testdata", "signtest"))
if err != nil {
t.Fatal(err)
}
if err := chartutil.SaveDir(signtest, filepath.Join(chartPath, "testdata")); err != nil {
t.Fatal(err)
}
local, err := loader.LoadDir(filepath.Join("testdata", "local-subchart"))
if err != nil {
t.Fatal(err)
}
if err := chartutil.SaveDir(local, filepath.Join(chartPath, "charts")); err != nil {
t.Fatal(err)
}
signDep := &chart.Dependency{
Name: signtest.Name(),
Repository: "file://./testdata/signtest",
Version: signtest.Metadata.Version,
}
localDep := &chart.Dependency{
Name: local.Name(),
Repository: "",
Version: local.Metadata.Version,
}
// create a 'tmpcharts' directory to test #5567
if err := os.MkdirAll(filepath.Join(chartPath, "tmpcharts"), 0755); err != nil {
t.Fatal(err)
}
if err := m.downloadAll([]*chart.Dependency{signDep, localDep}); err != nil {
t.Error(err)
}
}
func TestUpdateBeforeBuild(t *testing.T) { func TestUpdateBeforeBuild(t *testing.T) {
// Set up a fake repo // Set up a fake repo
srv, err := repotest.NewTempServerWithCleanup(t, "testdata/*.tgz*") srv, err := repotest.NewTempServerWithCleanup(t, "testdata/*.tgz*")

@ -1,7 +1,7 @@
apiVersion: v1 apiVersion: v1
entries: entries:
tlsfoo: tlsfoo:
- name: tlsfoo - name: tlsfoo
description: TLS FOO Chart description: TLS FOO Chart
home: https://helm.sh/helm home: https://helm.sh/helm
keywords: [] keywords: []

@ -97,7 +97,7 @@ const warnStartDelim = "HELM_ERR_START"
const warnEndDelim = "HELM_ERR_END" const warnEndDelim = "HELM_ERR_END"
const recursionMaxNums = 1000 const recursionMaxNums = 1000
var warnRegex = regexp.MustCompile(warnStartDelim + `(.*)` + warnEndDelim) var warnRegex = regexp.MustCompile(warnStartDelim + `((?s).*)` + warnEndDelim)
func warnWrap(warn string) string { func warnWrap(warn string) string {
return warnStartDelim + warn + warnEndDelim return warnStartDelim + warn + warnEndDelim
@ -347,8 +347,13 @@ func allTemplates(c *chart.Chart, vals chartutil.Values) map[string]renderable {
// scope. // scope.
func recAllTpls(c *chart.Chart, templates map[string]renderable, vals chartutil.Values) map[string]interface{} { func recAllTpls(c *chart.Chart, templates map[string]renderable, vals chartutil.Values) map[string]interface{} {
subCharts := make(map[string]interface{}) subCharts := make(map[string]interface{})
chartMetaData := struct {
chart.Metadata
IsRoot bool
}{*c.Metadata, c.IsRoot()}
next := map[string]interface{}{ next := map[string]interface{}{
"Chart": c.Metadata, "Chart": chartMetaData,
"Files": newFiles(c.Files), "Files": newFiles(c.Files),
"Release": vals["Release"], "Release": vals["Release"],
"Capabilities": vals["Capabilities"], "Capabilities": vals["Capabilities"],

@ -245,44 +245,65 @@ func TestParseErrors(t *testing.T) {
func TestExecErrors(t *testing.T) { func TestExecErrors(t *testing.T) {
vals := chartutil.Values{"Values": map[string]interface{}{}} vals := chartutil.Values{"Values": map[string]interface{}{}}
cases := []struct {
tplsMissingRequired := map[string]renderable{ name string
"missing_required": {tpl: `{{required "foo is required" .Values.foo}}`, vals: vals}, tpls map[string]renderable
} expected string
_, err := new(Engine).render(tplsMissingRequired) }{
if err == nil { {
t.Fatalf("Expected failures while rendering: %s", err) name: "MissingRequired",
} tpls: map[string]renderable{
expected := `execution error at (missing_required:1:2): foo is required` "missing_required": {tpl: `{{required "foo is required" .Values.foo}}`, vals: vals},
if err.Error() != expected { },
t.Errorf("Expected '%s', got %q", expected, err.Error()) expected: `execution error at (missing_required:1:2): foo is required`,
} },
{
tplsMissingRequired = map[string]renderable{ name: "MissingRequiredWithColons",
"missing_required_with_colons": {tpl: `{{required ":this: message: has many: colons:" .Values.foo}}`, vals: vals}, tpls: map[string]renderable{
} "missing_required_with_colons": {tpl: `{{required ":this: message: has many: colons:" .Values.foo}}`, vals: vals},
_, err = new(Engine).render(tplsMissingRequired) },
if err == nil { expected: `execution error at (missing_required_with_colons:1:2): :this: message: has many: colons:`,
t.Fatalf("Expected failures while rendering: %s", err) },
} {
expected = `execution error at (missing_required_with_colons:1:2): :this: message: has many: colons:` name: "Issue6044",
if err.Error() != expected { tpls: map[string]renderable{
t.Errorf("Expected '%s', got %q", expected, err.Error()) "issue6044": {
} vals: vals,
tpl: `{{ $someEmptyValue := "" }}
issue6044tpl := `{{ $someEmptyValue := "" }}
{{ $myvar := "abc" }} {{ $myvar := "abc" }}
{{- required (printf "%s: something is missing" $myvar) $someEmptyValue | repeat 0 }}` {{- required (printf "%s: something is missing" $myvar) $someEmptyValue | repeat 0 }}`,
tplsMissingRequired = map[string]renderable{ },
"issue6044": {tpl: issue6044tpl, vals: vals}, },
} expected: `execution error at (issue6044:3:4): abc: something is missing`,
_, err = new(Engine).render(tplsMissingRequired) },
if err == nil { {
t.Fatalf("Expected failures while rendering: %s", err) name: "MissingRequiredWithNewlines",
tpls: map[string]renderable{
"issue9981": {tpl: `{{required "foo is required\nmore info after the break" .Values.foo}}`, vals: vals},
},
expected: `execution error at (issue9981:1:2): foo is required
more info after the break`,
},
{
name: "FailWithNewlines",
tpls: map[string]renderable{
"issue9981": {tpl: `{{fail "something is wrong\nlinebreak"}}`, vals: vals},
},
expected: `execution error at (issue9981:1:2): something is wrong
linebreak`,
},
} }
expected = `execution error at (issue6044:3:4): abc: something is missing`
if err.Error() != expected { for _, tt := range cases {
t.Errorf("Expected '%s', got %q", expected, err.Error()) t.Run(tt.name, func(t *testing.T) {
_, err := new(Engine).render(tt.tpls)
if err == nil {
t.Fatalf("Expected failures while rendering: %s", err)
}
if err.Error() != tt.expected {
t.Errorf("Expected %q, got %q", tt.expected, err.Error())
}
})
} }
} }
@ -346,6 +367,36 @@ func TestAllTemplates(t *testing.T) {
} }
} }
func TestChartValuesContainsIsRoot(t *testing.T) {
ch1 := &chart.Chart{
Metadata: &chart.Metadata{Name: "parent"},
Templates: []*chart.File{
{Name: "templates/isroot", Data: []byte("{{.Chart.IsRoot}}")},
},
}
dep1 := &chart.Chart{
Metadata: &chart.Metadata{Name: "child"},
Templates: []*chart.File{
{Name: "templates/isroot", Data: []byte("{{.Chart.IsRoot}}")},
},
}
ch1.AddDependency(dep1)
out, err := Render(ch1, chartutil.Values{})
if err != nil {
t.Fatalf("failed to render templates: %s", err)
}
expects := map[string]string{
"parent/charts/child/templates/isroot": "false",
"parent/templates/isroot": "true",
}
for file, expect := range expects {
if out[file] != expect {
t.Errorf("Expected %q, got %q", expect, out[file])
}
}
}
func TestRenderDependency(t *testing.T) { func TestRenderDependency(t *testing.T) {
deptpl := `{{define "myblock"}}World{{end}}` deptpl := `{{define "myblock"}}World{{end}}`
toptpl := `Hello {{template "myblock"}}` toptpl := `Hello {{template "myblock"}}`

@ -227,7 +227,7 @@ func (c *ReadyChecker) jobReady(job *batchv1.Job) bool {
c.log("Job is failed: %s/%s", job.GetNamespace(), job.GetName()) c.log("Job is failed: %s/%s", job.GetNamespace(), job.GetName())
return false return false
} }
if job.Status.Succeeded < *job.Spec.Completions { if job.Spec.Completions != nil && job.Status.Succeeded < *job.Spec.Completions {
c.log("Job is not completed: %s/%s", job.GetNamespace(), job.GetName()) c.log("Job is not completed: %s/%s", job.GetNamespace(), job.GetName())
return false return false
} }

@ -241,39 +241,44 @@ func Test_ReadyChecker_jobReady(t *testing.T) {
}{ }{
{ {
name: "job is completed", name: "job is completed",
args: args{job: newJob("foo", 1, 1, 1, 0)}, args: args{job: newJob("foo", 1, intToInt32(1), 1, 0)},
want: true, want: true,
}, },
{ {
name: "job is incomplete", name: "job is incomplete",
args: args{job: newJob("foo", 1, 1, 0, 0)}, args: args{job: newJob("foo", 1, intToInt32(1), 0, 0)},
want: false, want: false,
}, },
{ {
name: "job is failed", name: "job is failed",
args: args{job: newJob("foo", 1, 1, 0, 1)}, args: args{job: newJob("foo", 1, intToInt32(1), 0, 1)},
want: false, want: false,
}, },
{ {
name: "job is completed with retry", name: "job is completed with retry",
args: args{job: newJob("foo", 1, 1, 1, 1)}, args: args{job: newJob("foo", 1, intToInt32(1), 1, 1)},
want: true, want: true,
}, },
{ {
name: "job is failed with retry", name: "job is failed with retry",
args: args{job: newJob("foo", 1, 1, 0, 2)}, args: args{job: newJob("foo", 1, intToInt32(1), 0, 2)},
want: false, want: false,
}, },
{ {
name: "job is completed single run", name: "job is completed single run",
args: args{job: newJob("foo", 0, 1, 1, 0)}, args: args{job: newJob("foo", 0, intToInt32(1), 1, 0)},
want: true, want: true,
}, },
{ {
name: "job is failed single run", name: "job is failed single run",
args: args{job: newJob("foo", 0, 1, 0, 1)}, args: args{job: newJob("foo", 0, intToInt32(1), 0, 1)},
want: false, want: false,
}, },
{
name: "job with null completions",
args: args{job: newJob("foo", 0, nil, 1, 0)},
want: true,
},
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
@ -481,7 +486,7 @@ func newPersistentVolumeClaim(name string, phase corev1.PersistentVolumeClaimPha
} }
} }
func newJob(name string, backoffLimit, completions, succeeded, failed int) *batchv1.Job { func newJob(name string, backoffLimit int, completions *int32, succeeded int, failed int) *batchv1.Job {
return &batchv1.Job{ return &batchv1.Job{
ObjectMeta: metav1.ObjectMeta{ ObjectMeta: metav1.ObjectMeta{
Name: name, Name: name,
@ -489,7 +494,7 @@ func newJob(name string, backoffLimit, completions, succeeded, failed int) *batc
}, },
Spec: batchv1.JobSpec{ Spec: batchv1.JobSpec{
BackoffLimit: intToInt32(backoffLimit), BackoffLimit: intToInt32(backoffLimit),
Completions: intToInt32(completions), Completions: completions,
Template: corev1.PodTemplateSpec{ Template: corev1.PodTemplateSpec{
ObjectMeta: metav1.ObjectMeta{ ObjectMeta: metav1.ObjectMeta{
Name: name, Name: name,

@ -146,6 +146,7 @@ func Templates(linter *support.Linter, values map[string]interface{}, namespace
linter.RunLinterRule(support.WarningSev, fpath, validateNoDeprecations(yamlStruct)) linter.RunLinterRule(support.WarningSev, fpath, validateNoDeprecations(yamlStruct))
linter.RunLinterRule(support.ErrorSev, fpath, validateMatchSelector(yamlStruct, renderedContent)) linter.RunLinterRule(support.ErrorSev, fpath, validateMatchSelector(yamlStruct, renderedContent))
linter.RunLinterRule(support.ErrorSev, fpath, validateListAnnotations(yamlStruct, renderedContent))
} }
} }
} }
@ -293,6 +294,28 @@ func validateMatchSelector(yamlStruct *K8sYamlStruct, manifest string) error {
} }
return nil return nil
} }
func validateListAnnotations(yamlStruct *K8sYamlStruct, manifest string) error {
if yamlStruct.Kind == "List" {
m := struct {
Items []struct {
Metadata struct {
Annotations map[string]string
}
}
}{}
if err := yaml.Unmarshal([]byte(manifest), &m); err != nil {
return validateYamlContent(err)
}
for _, i := range m.Items {
if _, ok := i.Metadata.Annotations["helm.sh/resource-policy"]; ok {
return errors.New("Annotation 'helm.sh/resource-policy' within List objects are ignored")
}
}
}
return nil
}
// K8sYamlStruct stubs a Kubernetes YAML file. // K8sYamlStruct stubs a Kubernetes YAML file.
// //

@ -424,3 +424,41 @@ func TestEmptyWithCommentsManifests(t *testing.T) {
t.Fatalf("Expected 0 lint errors, got %d", l) t.Fatalf("Expected 0 lint errors, got %d", l)
} }
} }
func TestValidateListAnnotations(t *testing.T) {
md := &K8sYamlStruct{
APIVersion: "v1",
Kind: "List",
Metadata: k8sYamlMetadata{
Name: "list",
},
}
manifest := `
apiVersion: v1
kind: List
items:
- apiVersion: v1
kind: ConfigMap
metadata:
annotations:
helm.sh/resource-policy: keep
`
if err := validateListAnnotations(md, manifest); err == nil {
t.Fatal("expected list with nested keep annotations to fail")
}
manifest = `
apiVersion: v1
kind: List
metadata:
annotations:
helm.sh/resource-policy: keep
items:
- apiVersion: v1
kind: ConfigMap
`
if err := validateListAnnotations(md, manifest); err != nil {
t.Fatalf("List objects keep annotations should pass. got: %s", err)
}
}

@ -286,7 +286,8 @@ func FindChartInAuthAndTLSAndPassRepoURL(repoURL, username, password, chartName,
// ResolveReferenceURL resolves refURL relative to baseURL. // ResolveReferenceURL resolves refURL relative to baseURL.
// If refURL is absolute, it simply returns refURL. // If refURL is absolute, it simply returns refURL.
func ResolveReferenceURL(baseURL, refURL string) (string, error) { func ResolveReferenceURL(baseURL, refURL string) (string, error) {
parsedBaseURL, err := url.Parse(baseURL) // We need a trailing slash for ResolveReference to work, but make sure there isn't already one
parsedBaseURL, err := url.Parse(strings.TrimSuffix(baseURL, "/") + "/")
if err != nil { if err != nil {
return "", errors.Wrapf(err, "failed to parse %s as URL", baseURL) return "", errors.Wrapf(err, "failed to parse %s as URL", baseURL)
} }
@ -296,8 +297,6 @@ func ResolveReferenceURL(baseURL, refURL string) (string, error) {
return "", errors.Wrapf(err, "failed to parse %s as URL", refURL) return "", errors.Wrapf(err, "failed to parse %s as URL", refURL)
} }
// We need a trailing slash for ResolveReference to work, but make sure there isn't already one
parsedBaseURL.Path = strings.TrimSuffix(parsedBaseURL.Path, "/") + "/"
return parsedBaseURL.ResolveReference(parsedRefURL).String(), nil return parsedBaseURL.ResolveReference(parsedRefURL).String(), nil
} }

@ -400,4 +400,12 @@ func TestResolveReferenceURL(t *testing.T) {
if chartURL != "https://charts.helm.sh/stable/nginx-0.2.0.tgz" { if chartURL != "https://charts.helm.sh/stable/nginx-0.2.0.tgz" {
t.Errorf("%s", chartURL) t.Errorf("%s", chartURL)
} }
chartURL, err = ResolveReferenceURL("http://localhost:8123/charts%2fwith%2fescaped%2fslash", "nginx-0.2.0.tgz")
if err != nil {
t.Errorf("%s", err)
}
if chartURL != "http://localhost:8123/charts%2fwith%2fescaped%2fslash/nginx-0.2.0.tgz" {
t.Errorf("%s", chartURL)
}
} }

@ -50,6 +50,8 @@ var (
ErrNoChartVersion = errors.New("no chart version found") ErrNoChartVersion = errors.New("no chart version found")
// ErrNoChartName indicates that a chart with the given name is not found. // ErrNoChartName indicates that a chart with the given name is not found.
ErrNoChartName = errors.New("no chart name found") ErrNoChartName = errors.New("no chart name found")
// ErrEmptyIndexYaml indicates that the content of index.yaml is empty.
ErrEmptyIndexYaml = errors.New("empty index.yaml file")
) )
// ChartVersions is a list of versioned chart references. // ChartVersions is a list of versioned chart references.
@ -326,6 +328,11 @@ func IndexDirectory(dir, baseURL string) (*IndexFile, error) {
// This will fail if API Version is not set (ErrNoAPIVersion) or if the unmarshal fails. // This will fail if API Version is not set (ErrNoAPIVersion) or if the unmarshal fails.
func loadIndex(data []byte, source string) (*IndexFile, error) { func loadIndex(data []byte, source string) (*IndexFile, error) {
i := &IndexFile{} i := &IndexFile{}
if len(data) == 0 {
return i, ErrEmptyIndexYaml
}
if err := yaml.UnmarshalStrict(data, i); err != nil { if err := yaml.UnmarshalStrict(data, i); err != nil {
return i, err return i, err
} }

@ -152,6 +152,12 @@ func TestLoadIndex_Duplicates(t *testing.T) {
} }
} }
func TestLoadIndex_Empty(t *testing.T) {
if _, err := loadIndex([]byte(""), "indexWithEmpty"); err == nil {
t.Errorf("Expected an error when index.yaml is empty.")
}
}
func TestLoadIndexFileAnnotations(t *testing.T) { func TestLoadIndexFileAnnotations(t *testing.T) {
i, err := LoadIndexFile(annotationstestfile) i, err := LoadIndexFile(annotationstestfile)
if err != nil { if err != nil {

@ -26,13 +26,13 @@ import (
"testing" "testing"
"time" "time"
auth "github.com/deislabs/oras/pkg/auth/docker" "github.com/distribution/distribution/v3/configuration"
"github.com/docker/distribution/configuration" "github.com/distribution/distribution/v3/registry"
"github.com/docker/distribution/registry" _ "github.com/distribution/distribution/v3/registry/auth/htpasswd" // used for docker test registry
_ "github.com/docker/distribution/registry/auth/htpasswd" // used for docker test registry _ "github.com/distribution/distribution/v3/registry/storage/driver/inmemory" // used for docker test registry
_ "github.com/docker/distribution/registry/storage/driver/inmemory" // used for docker test registry
"github.com/phayes/freeport" "github.com/phayes/freeport"
"golang.org/x/crypto/bcrypt" "golang.org/x/crypto/bcrypt"
auth "oras.land/oras-go/pkg/auth/docker"
"sigs.k8s.io/yaml" "sigs.k8s.io/yaml"
ociRegistry "helm.sh/helm/v3/internal/experimental/registry" ociRegistry "helm.sh/helm/v3/internal/experimental/registry"
@ -56,6 +56,23 @@ func NewTempServerWithCleanup(t *testing.T, glob string) (*Server, error) {
return srv, err return srv, err
} }
// Set up a fake repo with basic auth enabled
func NewTempServerWithCleanupAndBasicAuth(t *testing.T, glob string) *Server {
srv, err := NewTempServerWithCleanup(t, glob)
srv.Stop()
if err != nil {
t.Fatal(err)
}
srv.WithMiddleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
username, password, ok := r.BasicAuth()
if !ok || username != "username" || password != "password" {
t.Errorf("Expected request to use basic auth and for username == 'username' and password == 'password', got '%v', '%s', '%s'", ok, username, password)
}
}))
srv.Start()
return srv
}
type OCIServer struct { type OCIServer struct {
*registry.Registry *registry.Registry
RegistryURL string RegistryURL string

@ -55,8 +55,8 @@ func (s *Storage) Get(name string, version int) (*rspb.Release, error) {
} }
// Create creates a new storage entry holding the release. An // Create creates a new storage entry holding the release. An
// error is returned if the storage driver failed to store the // error is returned if the storage driver fails to store the
// release, or a release with identical an key already exists. // release, or a release with an identical key already exists.
func (s *Storage) Create(rls *rspb.Release) error { func (s *Storage) Create(rls *rspb.Release) error {
s.Log("creating release %q", makeKey(rls.Name, rls.Version)) s.Log("creating release %q", makeKey(rls.Name, rls.Version))
if s.MaxHistory > 0 { if s.MaxHistory > 0 {

Loading…
Cancel
Save