Merge remote-tracking branch 'helm/master'

pull/8034/head
Liu Ming 5 years ago
commit ee91c3e85f

@ -58,7 +58,7 @@ func loadPlugins(baseCmd *cobra.Command, out io.Writer) {
return
}
found, err := findPlugins(settings.PluginsDirectory)
found, err := plugin.FindPlugins(settings.PluginsDirectory)
if err != nil {
fmt.Fprintf(os.Stderr, "failed to load plugins: %s", err)
return
@ -238,20 +238,6 @@ func manuallyProcessArgs(args []string) ([]string, []string) {
return known, unknown
}
// findPlugins returns a list of YAML files that describe plugins.
func findPlugins(plugdirs string) ([]*plugin.Plugin, error) {
found := []*plugin.Plugin{}
// Let's get all UNIXy and allow path separators
for _, p := range filepath.SplitList(plugdirs) {
matches, err := plugin.LoadAll(p)
if err != nil {
return matches, err
}
found = append(found, matches...)
}
return found, nil
}
// pluginCommand represents the optional completion.yaml file of a plugin
type pluginCommand struct {
Name string `json:"name"`

@ -22,6 +22,8 @@ import (
"github.com/gosuri/uitable"
"github.com/spf13/cobra"
"helm.sh/helm/v3/pkg/plugin"
)
func newPluginListCmd(out io.Writer) *cobra.Command {
@ -31,7 +33,7 @@ func newPluginListCmd(out io.Writer) *cobra.Command {
Short: "list installed Helm plugins",
RunE: func(cmd *cobra.Command, args []string) error {
debug("pluginDirs: %s", settings.PluginsDirectory)
plugins, err := findPlugins(settings.PluginsDirectory)
plugins, err := plugin.FindPlugins(settings.PluginsDirectory)
if err != nil {
return err
}
@ -51,7 +53,7 @@ func newPluginListCmd(out io.Writer) *cobra.Command {
// Provide dynamic auto-completion for plugin names
func compListPlugins(toComplete string) []string {
var pNames []string
plugins, err := findPlugins(settings.PluginsDirectory)
plugins, err := plugin.FindPlugins(settings.PluginsDirectory)
if err == nil {
for _, p := range plugins {
if strings.HasPrefix(p.Metadata.Name, toComplete) {

@ -68,7 +68,7 @@ func (o *pluginUninstallOptions) complete(args []string) error {
func (o *pluginUninstallOptions) run(out io.Writer) error {
debug("loading installed plugins from %s", settings.PluginsDirectory)
plugins, err := findPlugins(settings.PluginsDirectory)
plugins, err := plugin.FindPlugins(settings.PluginsDirectory)
if err != nil {
return err
}

@ -70,7 +70,7 @@ func (o *pluginUpdateOptions) complete(args []string) error {
func (o *pluginUpdateOptions) run(out io.Writer) error {
installer.Debug = settings.Debug
debug("loading installed plugins from %s", settings.PluginsDirectory)
plugins, err := findPlugins(settings.PluginsDirectory)
plugins, err := plugin.FindPlugins(settings.PluginsDirectory)
if err != nil {
return err
}

@ -35,7 +35,6 @@ import (
"io/ioutil"
"os"
"os/exec"
"os/user"
"path/filepath"
"runtime"
"sync"
@ -175,13 +174,9 @@ func TestCopyDirFail_SrcInaccessible(t *testing.T) {
t.Skip("skipping on windows")
}
var currentUser, err = user.Current()
var currentUID = os.Getuid()
if err != nil {
t.Fatalf("Failed to get name of current user: %s", err)
}
if currentUser.Name == "root" {
if currentUID == 0 {
// Skipping if root, because all files are accessible
t.Skip("Skipping for root user")
}
@ -214,13 +209,9 @@ func TestCopyDirFail_DstInaccessible(t *testing.T) {
t.Skip("skipping on windows")
}
var currentUser, err = user.Current()
var currentUID = os.Getuid()
if err != nil {
t.Fatalf("Failed to get name of current user: %s", err)
}
if currentUser.Name == "root" {
if currentUID == 0 {
// Skipping if root, because all files are accessible
t.Skip("Skipping for root user")
}
@ -314,13 +305,9 @@ func TestCopyDirFailOpen(t *testing.T) {
t.Skip("skipping on windows")
}
var currentUser, err = user.Current()
if err != nil {
t.Fatalf("Failed to get name of current user: %s", err)
}
var currentUID = os.Getuid()
if currentUser.Name == "root" {
if currentUID == 0 {
// Skipping if root, because all files are accessible
t.Skip("Skipping for root user")
}
@ -483,13 +470,9 @@ func TestCopyFileFail(t *testing.T) {
t.Skip("skipping on windows")
}
var currentUser, err = user.Current()
var currentUID = os.Getuid()
if err != nil {
t.Fatalf("Failed to get name of current user: %s", err)
}
if currentUser.Name == "root" {
if currentUID == 0 {
// Skipping if root, because all files are accessible
t.Skip("Skipping for root user")
}
@ -574,13 +557,9 @@ func setupInaccessibleDir(t *testing.T, op func(dir string) error) func() {
func TestIsDir(t *testing.T) {
var currentUser, err = user.Current()
var currentUID = os.Getuid()
if err != nil {
t.Fatalf("Failed to get name of current user: %s", err)
}
if currentUser.Name == "root" {
if currentUID == 0 {
// Skipping if root, because all files are accessible
t.Skip("Skipping for root user")
}
@ -631,13 +610,9 @@ func TestIsDir(t *testing.T) {
func TestIsSymlink(t *testing.T) {
var currentUser, err = user.Current()
if err != nil {
t.Fatalf("Failed to get name of current user: %s", err)
}
var currentUID = os.Getuid()
if currentUser.Name == "root" {
if currentUID == 0 {
// Skipping if root, because all files are accessible
t.Skip("Skipping for root user")
}

@ -17,10 +17,13 @@ limitations under the License.
package action
import (
"bytes"
"fmt"
"os"
"path"
"path/filepath"
"regexp"
"strings"
"github.com/pkg/errors"
"k8s.io/apimachinery/pkg/api/meta"
@ -30,9 +33,13 @@ import (
"k8s.io/client-go/rest"
"helm.sh/helm/v3/internal/experimental/registry"
"helm.sh/helm/v3/pkg/chart"
"helm.sh/helm/v3/pkg/chartutil"
"helm.sh/helm/v3/pkg/engine"
"helm.sh/helm/v3/pkg/kube"
"helm.sh/helm/v3/pkg/postrender"
"helm.sh/helm/v3/pkg/release"
"helm.sh/helm/v3/pkg/releaseutil"
"helm.sh/helm/v3/pkg/storage"
"helm.sh/helm/v3/pkg/storage/driver"
"helm.sh/helm/v3/pkg/time"
@ -86,6 +93,132 @@ type Configuration struct {
Log func(string, ...interface{})
}
// renderResources renders the templates in a chart
//
// TODO: This function is badly in need of a refactor.
func (c *Configuration) renderResources(ch *chart.Chart, values chartutil.Values, releaseName, outputDir string, subNotes, useReleaseName, includeCrds bool, pr postrender.PostRenderer, dryRun bool) ([]*release.Hook, *bytes.Buffer, string, error) {
hs := []*release.Hook{}
b := bytes.NewBuffer(nil)
caps, err := c.getCapabilities()
if err != nil {
return hs, b, "", err
}
if ch.Metadata.KubeVersion != "" {
if !chartutil.IsCompatibleRange(ch.Metadata.KubeVersion, caps.KubeVersion.String()) {
return hs, b, "", errors.Errorf("chart requires kubeVersion: %s which is incompatible with Kubernetes %s", ch.Metadata.KubeVersion, caps.KubeVersion.String())
}
}
var files map[string]string
var err2 error
// A `helm template` or `helm install --dry-run` should not talk to the remote cluster.
// It will break in interesting and exotic ways because other data (e.g. discovery)
// is mocked. It is not up to the template author to decide when the user wants to
// connect to the cluster. So when the user says to dry run, respect the user's
// wishes and do not connect to the cluster.
if !dryRun && c.RESTClientGetter != nil {
rest, err := c.RESTClientGetter.ToRESTConfig()
if err != nil {
return hs, b, "", err
}
files, err2 = engine.RenderWithClient(ch, values, rest)
} else {
files, err2 = engine.Render(ch, values)
}
if err2 != nil {
return hs, b, "", err2
}
// NOTES.txt gets rendered like all the other files, but because it's not a hook nor a resource,
// pull it out of here into a separate file so that we can actually use the output of the rendered
// text file. We have to spin through this map because the file contains path information, so we
// look for terminating NOTES.txt. We also remove it from the files so that we don't have to skip
// it in the sortHooks.
var notesBuffer bytes.Buffer
for k, v := range files {
if strings.HasSuffix(k, notesFileSuffix) {
if subNotes || (k == path.Join(ch.Name(), "templates", notesFileSuffix)) {
// If buffer contains data, add newline before adding more
if notesBuffer.Len() > 0 {
notesBuffer.WriteString("\n")
}
notesBuffer.WriteString(v)
}
delete(files, k)
}
}
notes := notesBuffer.String()
// Sort hooks, manifests, and partials. Only hooks and manifests are returned,
// as partials are not used after renderer.Render. Empty manifests are also
// removed here.
hs, manifests, err := releaseutil.SortManifests(files, caps.APIVersions, releaseutil.InstallOrder)
if err != nil {
// By catching parse errors here, we can prevent bogus releases from going
// to Kubernetes.
//
// We return the files as a big blob of data to help the user debug parser
// errors.
for name, content := range files {
if strings.TrimSpace(content) == "" {
continue
}
fmt.Fprintf(b, "---\n# Source: %s\n%s\n", name, content)
}
return hs, b, "", err
}
// Aggregate all valid manifests into one big doc.
fileWritten := make(map[string]bool)
if includeCrds {
for _, crd := range ch.CRDObjects() {
if outputDir == "" {
fmt.Fprintf(b, "---\n# Source: %s\n%s\n", crd.Name, string(crd.File.Data[:]))
} else {
err = writeToFile(outputDir, crd.Filename, string(crd.File.Data[:]), fileWritten[crd.Name])
if err != nil {
return hs, b, "", err
}
fileWritten[crd.Name] = true
}
}
}
for _, m := range manifests {
if outputDir == "" {
fmt.Fprintf(b, "---\n# Source: %s\n%s\n", m.Name, m.Content)
} else {
newDir := outputDir
if useReleaseName {
newDir = filepath.Join(outputDir, releaseName)
}
// NOTE: We do not have to worry about the post-renderer because
// output dir is only used by `helm template`. In the next major
// release, we should move this logic to template only as it is not
// used by install or upgrade
err = writeToFile(newDir, m.Name, m.Content, fileWritten[m.Name])
if err != nil {
return hs, b, "", err
}
fileWritten[m.Name] = true
}
}
if pr != nil {
b, err = pr.Run(b)
if err != nil {
return hs, b, notes, errors.Wrap(err, "error while running post render on files")
}
}
return hs, b, notes, nil
}
// RESTClientGetter gets the rest client
type RESTClientGetter interface {
ToRESTConfig() (*rest.Config, error)

@ -39,7 +39,6 @@ import (
"helm.sh/helm/v3/pkg/chartutil"
"helm.sh/helm/v3/pkg/cli"
"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"
@ -232,7 +231,7 @@ func (i *Install) Run(chrt *chart.Chart, vals map[string]interface{}) (*release.
rel := i.createRelease(chrt, vals)
var manifestDoc *bytes.Buffer
rel.Hooks, manifestDoc, rel.Info.Notes, err = i.cfg.renderResources(chrt, valuesToRender, i.ReleaseName, i.OutputDir, i.SubNotes, i.UseReleaseName, i.IncludeCRDs, i.PostRenderer)
rel.Hooks, manifestDoc, rel.Info.Notes, err = i.cfg.renderResources(chrt, valuesToRender, i.ReleaseName, i.OutputDir, i.SubNotes, i.UseReleaseName, i.IncludeCRDs, i.PostRenderer, i.DryRun)
// Even for errors, attach this if available
if manifestDoc != nil {
rel.Manifest = manifestDoc.String()
@ -475,125 +474,6 @@ func (i *Install) replaceRelease(rel *release.Release) error {
return i.recordRelease(last)
}
// renderResources renders the templates in a chart
func (c *Configuration) renderResources(ch *chart.Chart, values chartutil.Values, releaseName, outputDir string, subNotes, useReleaseName, includeCrds bool, pr postrender.PostRenderer) ([]*release.Hook, *bytes.Buffer, string, error) {
hs := []*release.Hook{}
b := bytes.NewBuffer(nil)
caps, err := c.getCapabilities()
if err != nil {
return hs, b, "", err
}
if ch.Metadata.KubeVersion != "" {
if !chartutil.IsCompatibleRange(ch.Metadata.KubeVersion, caps.KubeVersion.String()) {
return hs, b, "", errors.Errorf("chart requires kubeVersion: %s which is incompatible with Kubernetes %s", ch.Metadata.KubeVersion, caps.KubeVersion.String())
}
}
var files map[string]string
var err2 error
if c.RESTClientGetter != nil {
rest, err := c.RESTClientGetter.ToRESTConfig()
if err != nil {
return hs, b, "", err
}
files, err2 = engine.RenderWithClient(ch, values, rest)
} else {
files, err2 = engine.Render(ch, values)
}
if err2 != nil {
return hs, b, "", err2
}
// NOTES.txt gets rendered like all the other files, but because it's not a hook nor a resource,
// pull it out of here into a separate file so that we can actually use the output of the rendered
// text file. We have to spin through this map because the file contains path information, so we
// look for terminating NOTES.txt. We also remove it from the files so that we don't have to skip
// it in the sortHooks.
var notesBuffer bytes.Buffer
for k, v := range files {
if strings.HasSuffix(k, notesFileSuffix) {
if subNotes || (k == path.Join(ch.Name(), "templates", notesFileSuffix)) {
// If buffer contains data, add newline before adding more
if notesBuffer.Len() > 0 {
notesBuffer.WriteString("\n")
}
notesBuffer.WriteString(v)
}
delete(files, k)
}
}
notes := notesBuffer.String()
// Sort hooks, manifests, and partials. Only hooks and manifests are returned,
// as partials are not used after renderer.Render. Empty manifests are also
// removed here.
hs, manifests, err := releaseutil.SortManifests(files, caps.APIVersions, releaseutil.InstallOrder)
if err != nil {
// By catching parse errors here, we can prevent bogus releases from going
// to Kubernetes.
//
// We return the files as a big blob of data to help the user debug parser
// errors.
for name, content := range files {
if strings.TrimSpace(content) == "" {
continue
}
fmt.Fprintf(b, "---\n# Source: %s\n%s\n", name, content)
}
return hs, b, "", err
}
// Aggregate all valid manifests into one big doc.
fileWritten := make(map[string]bool)
if includeCrds {
for _, crd := range ch.CRDObjects() {
if outputDir == "" {
fmt.Fprintf(b, "---\n# Source: %s\n%s\n", crd.Name, string(crd.File.Data[:]))
} else {
err = writeToFile(outputDir, crd.Filename, string(crd.File.Data[:]), fileWritten[crd.Name])
if err != nil {
return hs, b, "", err
}
fileWritten[crd.Name] = true
}
}
}
for _, m := range manifests {
if outputDir == "" {
fmt.Fprintf(b, "---\n# Source: %s\n%s\n", m.Name, m.Content)
} else {
newDir := outputDir
if useReleaseName {
newDir = filepath.Join(outputDir, releaseName)
}
// NOTE: We do not have to worry about the post-renderer because
// output dir is only used by `helm template`. In the next major
// release, we should move this logic to template only as it is not
// used by install or upgrade
err = writeToFile(newDir, m.Name, m.Content, fileWritten[m.Name])
if err != nil {
return hs, b, "", err
}
fileWritten[m.Name] = true
}
}
if pr != nil {
b, err = pr.Run(b)
if err != nil {
return hs, b, notes, errors.Wrap(err, "error while running post render on files")
}
}
return hs, b, notes, nil
}
// write the <data> to <output-dir>/<name>. <append> controls if the file is created or content will be appended
func writeToFile(outputDir string, name string, data string, append bool) error {
outfileName := strings.Join([]string{outputDir, name}, string(filepath.Separator))

@ -29,6 +29,7 @@ import (
"github.com/stretchr/testify/assert"
"helm.sh/helm/v3/internal/test"
"helm.sh/helm/v3/pkg/chart"
"helm.sh/helm/v3/pkg/chartutil"
kubefake "helm.sh/helm/v3/pkg/kube/fake"
"helm.sh/helm/v3/pkg/release"
@ -240,6 +241,27 @@ func TestInstallRelease_DryRun(t *testing.T) {
is.Equal(res.Info.Description, "Dry run complete")
}
// Regression test for #7955: Lookup must not connect to Kubernetes on a dry-run.
func TestInstallRelease_DryRun_Lookup(t *testing.T) {
is := assert.New(t)
instAction := installAction(t)
instAction.DryRun = true
vals := map[string]interface{}{}
mockChart := buildChart(withSampleTemplates())
mockChart.Templates = append(mockChart.Templates, &chart.File{
Name: "templates/lookup",
Data: []byte(`goodbye: {{ lookup "v1" "Namespace" "" "___" }}`),
})
res, err := instAction.Run(mockChart, vals)
if err != nil {
t.Fatalf("Failed install: %s", err)
}
is.Contains(res.Manifest, "goodbye: map[]")
}
func TestInstallReleaseIncorrectTemplate_DryRun(t *testing.T) {
is := assert.New(t)
instAction := installAction(t)

@ -217,7 +217,7 @@ func (u *Upgrade) prepareUpgrade(name string, chart *chart.Chart, vals map[strin
return nil, nil, err
}
hooks, manifestDoc, notesTxt, err := u.cfg.renderResources(chart, valuesToRender, "", "", u.SubNotes, false, false, u.PostRenderer)
hooks, manifestDoc, notesTxt, err := u.cfg.renderResources(chart, valuesToRender, "", "", u.SubNotes, false, false, u.PostRenderer, u.DryRun)
if err != nil {
return nil, nil, err
}

@ -175,7 +175,11 @@ func coalesceValues(c *chart.Chart, v map[string]interface{}) {
//
// dest is considered authoritative.
func CoalesceTables(dst, src map[string]interface{}) map[string]interface{} {
if dst == nil || src == nil {
// When --reuse-values is set but there are no modifications yet, return new values
if src == nil {
return dst
}
if dst == nil {
return src
}
// Because dest has higher precedence than src, dest values override src

@ -211,4 +211,57 @@ func TestCoalesceTables(t *testing.T) {
if _, ok = dst["hole"]; ok {
t.Error("The hole still exists.")
}
dst2 := map[string]interface{}{
"name": "Ishmael",
"address": map[string]interface{}{
"street": "123 Spouter Inn Ct.",
"city": "Nantucket",
"country": "US",
},
"details": map[string]interface{}{
"friends": []string{"Tashtego"},
},
"boat": "pequod",
"hole": "black",
}
// What we expect is that anything in dst should have all values set,
// this happens when the --reuse-values flag is set but the chart has no modifications yet
CoalesceTables(dst2, nil)
if dst2["name"] != "Ishmael" {
t.Errorf("Unexpected name: %s", dst2["name"])
}
addr2, ok := dst2["address"].(map[string]interface{})
if !ok {
t.Fatal("Address went away.")
}
if addr2["street"].(string) != "123 Spouter Inn Ct." {
t.Errorf("Unexpected address: %v", addr2["street"])
}
if addr2["city"].(string) != "Nantucket" {
t.Errorf("Unexpected city: %v", addr2["city"])
}
if addr2["country"].(string) != "US" {
t.Errorf("Unexpected Country: %v", addr2["country"])
}
if det2, ok := dst2["details"].(map[string]interface{}); !ok {
t.Fatalf("Details is the wrong type: %v", dst2["details"])
} else if _, ok := det2["friends"]; !ok {
t.Error("Could not find your friends. Maybe you don't have any. :-(")
}
if dst2["boat"].(string) != "pequod" {
t.Errorf("Expected boat string, got %v", dst2["boat"])
}
if dst2["hole"].(string) != "black" {
t.Errorf("Expected hole string, got %v", dst2["boat"])
}
}

@ -172,7 +172,10 @@ func (e Engine) initFunMap(t *template.Template, referenceTpls map[string]render
}
return val, nil
}
if e.config != nil {
// If we are not linting and have a cluster connection, provide a Kubernetes-backed
// implementation.
if !e.LintMode && e.config != nil {
funcMap["lookup"] = NewLookupFunction(e.config)
}
@ -213,6 +216,7 @@ func (e Engine) renderWithReferences(tpls, referenceTpls map[string]renderable)
// We want to parse the templates in a predictable order. The order favors
// higher-level (in file system) templates over deeply nested templates.
keys := sortTemplates(tpls)
referenceKeys := sortTemplates(referenceTpls)
for _, filename := range keys {
r := tpls[filename]
@ -223,8 +227,9 @@ func (e Engine) renderWithReferences(tpls, referenceTpls map[string]renderable)
// Adding the reference templates to the template context
// so they can be referenced in the tpl function
for filename, r := range referenceTpls {
for _, filename := range referenceKeys {
if t.Lookup(filename) == nil {
r := referenceTpls[filename]
if _, err := t.New(filename).Parse(r.tpl); err != nil {
return map[string]string{}, cleanupParseError(filename, err)
}

@ -70,7 +70,7 @@ func TestFuncMap(t *testing.T) {
}
// Test for Engine-specific template functions.
expect := []string{"include", "required", "tpl", "toYaml", "fromYaml", "toToml", "toJson", "fromJson"}
expect := []string{"include", "required", "tpl", "toYaml", "fromYaml", "toToml", "toJson", "fromJson", "lookup"}
for _, f := range expect {
if _, ok := fns[f]; !ok {
t.Errorf("Expected add-on function %q", f)
@ -126,6 +126,46 @@ func TestRender(t *testing.T) {
}
}
func TestRenderRefsOrdering(t *testing.T) {
parentChart := &chart.Chart{
Metadata: &chart.Metadata{
Name: "parent",
Version: "1.2.3",
},
Templates: []*chart.File{
{Name: "templates/_helpers.tpl", Data: []byte(`{{- define "test" -}}parent value{{- end -}}`)},
{Name: "templates/test.yaml", Data: []byte(`{{ tpl "{{ include \"test\" . }}" . }}`)},
},
}
childChart := &chart.Chart{
Metadata: &chart.Metadata{
Name: "child",
Version: "1.2.3",
},
Templates: []*chart.File{
{Name: "templates/_helpers.tpl", Data: []byte(`{{- define "test" -}}child value{{- end -}}`)},
},
}
parentChart.AddDependency(childChart)
expect := map[string]string{
"parent/templates/test.yaml": "parent value",
}
for i := 0; i < 100; i++ {
out, err := Render(parentChart, chartutil.Values{})
if err != nil {
t.Fatalf("Failed to render templates: %s", err)
}
for name, data := range expect {
if out[name] != data {
t.Fatalf("Expected %q, got %q (iteraction %d)", data, out[name], i+1)
}
}
}
}
func TestRenderInternals(t *testing.T) {
// Test the internals of the rendering tool.

@ -62,6 +62,11 @@ func funcMap() template.FuncMap {
"include": func(string, interface{}) string { return "not implemented" },
"tpl": func(string, interface{}) interface{} { return "not implemented" },
"required": func(string, interface{}) (interface{}, error) { return "not implemented", nil },
// Provide a placeholder for the "lookup" function, which requires a kubernetes
// connection.
"lookup": func(string, string, string, string) (map[string]interface{}, error) {
return map[string]interface{}{}, nil
},
}
for k, v := range extra {

@ -94,6 +94,11 @@ func TestFuncs(t *testing.T) {
tpl: `{{ fromYamlArray . }}`,
expect: `[error unmarshaling JSON: while decoding JSON: json: cannot unmarshal object into Go value of type []interface {}]`,
vars: `hello: world`,
}, {
// This should never result in a network lookup. Regression for #7955
tpl: `{{ lookup "v1" "Namespace" "" "unlikelynamespace99999999" }}`,
expect: `map[]`,
vars: `["one", "two"]`,
}}
for _, tt := range tests {

@ -32,8 +32,12 @@ import (
type lookupFunc = func(apiversion string, resource string, namespace string, name string) (map[string]interface{}, error)
// NewLookupFunction returns a function for looking up objects in the cluster. If the resource does not exist, no error
// is raised.
// NewLookupFunction returns a function for looking up objects in the cluster.
//
// If the resource does not exist, no error is raised.
//
// This function is considered deprecated, and will be renamed in Helm 4. It will no
// longer be a public function.
func NewLookupFunction(config *rest.Config) lookupFunc {
return func(apiversion string, resource string, namespace string, name string) (map[string]interface{}, error) {
var client dynamic.ResourceInterface

@ -16,7 +16,6 @@ limitations under the License.
package installer // import "helm.sh/helm/v3/pkg/plugin/installer"
import (
"os"
"path/filepath"
"helm.sh/helm/v3/pkg/helmpath"
@ -31,13 +30,7 @@ func newBase(source string) base {
return base{source}
}
// link creates a symlink from the plugin source to the base path.
func (b *base) link(from string) error {
debug("symlinking %s to %s", from, b.Path())
return os.Symlink(from, b.Path())
}
// Path is where the plugin will be symlinked to.
// Path is where the plugin will be installed.
func (b *base) Path() string {
if b.Source == "" {
return ""

@ -27,6 +27,7 @@ import (
"github.com/pkg/errors"
"helm.sh/helm/v3/internal/third_party/dep/fs"
"helm.sh/helm/v3/pkg/cli"
"helm.sh/helm/v3/pkg/getter"
"helm.sh/helm/v3/pkg/helmpath"
@ -68,7 +69,6 @@ func NewExtractor(source string) (Extractor, error) {
// NewHTTPInstaller creates a new HttpInstaller.
func NewHTTPInstaller(source string) (*HTTPInstaller, error) {
key, err := cache.Key(source)
if err != nil {
return nil, err
@ -108,18 +108,16 @@ func stripPluginName(name string) string {
}
// Install downloads and extracts the tarball into the cache directory
// and creates a symlink to the plugin directory.
// and installs into the plugin directory.
//
// Implements Installer.
func (i *HTTPInstaller) Install() error {
pluginData, err := i.getter.Get(i.Source)
if err != nil {
return err
}
err = i.extractor.Extract(pluginData, i.CacheDir)
if err != nil {
if err := i.extractor.Extract(pluginData, i.CacheDir); err != nil {
return err
}
@ -132,7 +130,8 @@ func (i *HTTPInstaller) Install() error {
return err
}
return i.link(src)
debug("copying %s to %s", src, i.Path())
return fs.CopyDir(src, i.Path())
}
// Update updates a local repository
@ -141,12 +140,6 @@ func (i *HTTPInstaller) Update() error {
return errors.Errorf("method Update() not implemented for HttpInstaller")
}
// Override link because we want to use HttpInstaller.Path() not base.Path()
func (i *HTTPInstaller) link(from string) error {
debug("symlinking %s to %s", from, i.Path())
return os.Symlink(from, i.Path())
}
// Path is overridden because we want to join on the plugin name not the file name
func (i HTTPInstaller) Path() string {
if i.base.Source == "" {
@ -164,17 +157,16 @@ func (g *TarGzExtractor) Extract(buffer *bytes.Buffer, targetDir string) error {
return err
}
tarReader := tar.NewReader(uncompressedStream)
os.MkdirAll(targetDir, 0755)
if err := os.MkdirAll(targetDir, 0755); err != nil {
return err
}
tarReader := tar.NewReader(uncompressedStream)
for {
header, err := tarReader.Next()
if err == io.EOF {
break
}
if err != nil {
return err
}
@ -200,7 +192,5 @@ func (g *TarGzExtractor) Extract(buffer *bytes.Buffer, targetDir string) error {
return errors.Errorf("unknown type: %b in %s", header.Typeflag, header.Name)
}
}
return nil
}

@ -73,13 +73,13 @@ func TestHTTPInstaller(t *testing.T) {
i, err := NewForSource(source, "0.0.1")
if err != nil {
t.Errorf("unexpected error: %s", err)
t.Fatalf("unexpected error: %s", err)
}
// ensure a HTTPInstaller was returned
httpInstaller, ok := i.(*HTTPInstaller)
if !ok {
t.Error("expected a HTTPInstaller")
t.Fatal("expected a HTTPInstaller")
}
// inject fake http client responding with minimal plugin tarball
@ -94,17 +94,17 @@ func TestHTTPInstaller(t *testing.T) {
// install the plugin
if err := Install(i); err != nil {
t.Error(err)
t.Fatal(err)
}
if i.Path() != helmpath.DataPath("plugins", "fake-plugin") {
t.Errorf("expected path '$XDG_CONFIG_HOME/helm/plugins/fake-plugin', got %q", i.Path())
t.Fatalf("expected path '$XDG_CONFIG_HOME/helm/plugins/fake-plugin', got %q", i.Path())
}
// Install again to test plugin exists error
if err := Install(i); err == nil {
t.Error("expected error for plugin exists, got none")
t.Fatal("expected error for plugin exists, got none")
} else if err.Error() != "plugin already exists" {
t.Errorf("expected error for plugin exists, got (%v)", err)
t.Fatalf("expected error for plugin exists, got (%v)", err)
}
}
@ -119,13 +119,13 @@ func TestHTTPInstallerNonExistentVersion(t *testing.T) {
i, err := NewForSource(source, "0.0.2")
if err != nil {
t.Errorf("unexpected error: %s", err)
t.Fatalf("unexpected error: %s", err)
}
// ensure a HTTPInstaller was returned
httpInstaller, ok := i.(*HTTPInstaller)
if !ok {
t.Error("expected a HTTPInstaller")
t.Fatal("expected a HTTPInstaller")
}
// inject fake http client responding with error
@ -135,7 +135,7 @@ func TestHTTPInstallerNonExistentVersion(t *testing.T) {
// attempt to install the plugin
if err := Install(i); err == nil {
t.Error("expected error from http client")
t.Fatal("expected error from http client")
}
}
@ -150,13 +150,13 @@ func TestHTTPInstallerUpdate(t *testing.T) {
i, err := NewForSource(source, "0.0.1")
if err != nil {
t.Errorf("unexpected error: %s", err)
t.Fatalf("unexpected error: %s", err)
}
// ensure a HTTPInstaller was returned
httpInstaller, ok := i.(*HTTPInstaller)
if !ok {
t.Error("expected a HTTPInstaller")
t.Fatal("expected a HTTPInstaller")
}
// inject fake http client responding with minimal plugin tarball
@ -171,15 +171,15 @@ func TestHTTPInstallerUpdate(t *testing.T) {
// install the plugin before updating
if err := Install(i); err != nil {
t.Error(err)
t.Fatal(err)
}
if i.Path() != helmpath.DataPath("plugins", "fake-plugin") {
t.Errorf("expected path '$XDG_CONFIG_HOME/helm/plugins/fake-plugin', got %q", i.Path())
t.Fatalf("expected path '$XDG_CONFIG_HOME/helm/plugins/fake-plugin', got %q", i.Path())
}
// Update plugin, should fail because it is not implemented
if err := Update(i); err == nil {
t.Error("update method not implemented for http installer")
t.Fatal("update method not implemented for http installer")
}
}
@ -240,29 +240,27 @@ func TestExtract(t *testing.T) {
}
if err = extractor.Extract(&buf, tempDir); err != nil {
t.Errorf("Did not expect error but got error: %v", err)
t.Fatalf("Did not expect error but got error: %v", err)
}
pluginYAMLFullPath := filepath.Join(tempDir, "plugin.yaml")
if info, err := os.Stat(pluginYAMLFullPath); err != nil {
if os.IsNotExist(err) {
t.Errorf("Expected %s to exist but doesn't", pluginYAMLFullPath)
} else {
t.Error(err)
t.Fatalf("Expected %s to exist but doesn't", pluginYAMLFullPath)
}
t.Fatal(err)
} else if info.Mode().Perm() != 0600 {
t.Errorf("Expected %s to have 0600 mode it but has %o", pluginYAMLFullPath, info.Mode().Perm())
t.Fatalf("Expected %s to have 0600 mode it but has %o", pluginYAMLFullPath, info.Mode().Perm())
}
readmeFullPath := filepath.Join(tempDir, "README.md")
if info, err := os.Stat(readmeFullPath); err != nil {
if os.IsNotExist(err) {
t.Errorf("Expected %s to exist but doesn't", readmeFullPath)
} else {
t.Error(err)
t.Fatalf("Expected %s to exist but doesn't", readmeFullPath)
}
t.Fatal(err)
} else if info.Mode().Perm() != 0777 {
t.Errorf("Expected %s to have 0777 mode it but has %o", readmeFullPath, info.Mode().Perm())
t.Fatalf("Expected %s to have 0777 mode it but has %o", readmeFullPath, info.Mode().Perm())
}
}

@ -17,6 +17,7 @@ package installer
import (
"fmt"
"log"
"os"
"path/filepath"
"strings"
@ -103,9 +104,10 @@ func isPlugin(dirname string) bool {
return err == nil
}
var logger = log.New(os.Stderr, "[debug] ", log.Lshortfile)
func debug(format string, args ...interface{}) {
if Debug {
format = fmt.Sprintf("[debug] %s\n", format)
fmt.Printf(format, args...)
logger.Output(2, fmt.Sprintf(format, args...))
}
}

@ -16,6 +16,7 @@ limitations under the License.
package installer // import "helm.sh/helm/v3/pkg/plugin/installer"
import (
"os"
"path/filepath"
"github.com/pkg/errors"
@ -45,7 +46,8 @@ func (i *LocalInstaller) Install() error {
if !isPlugin(i.Source) {
return ErrMissingMetadata
}
return i.link(i.Source)
debug("symlinking %s to %s", i.Source, i.Path())
return os.Symlink(i.Source, i.Path())
}
// Update updates a local repository

@ -40,7 +40,7 @@ func TestLocalInstaller(t *testing.T) {
source := "../testdata/plugdir/echo"
i, err := NewForSource(source, "")
if err != nil {
t.Errorf("unexpected error: %s", err)
t.Fatalf("unexpected error: %s", err)
}
if err := Install(i); err != nil {
@ -48,6 +48,6 @@ func TestLocalInstaller(t *testing.T) {
}
if i.Path() != helmpath.DataPath("plugins", "echo") {
t.Errorf("expected path '$XDG_CONFIG_HOME/helm/plugins/helm-env', got %q", i.Path())
t.Fatalf("expected path '$XDG_CONFIG_HOME/helm/plugins/helm-env', got %q", i.Path())
}
}

@ -23,6 +23,7 @@ import (
"github.com/Masterminds/vcs"
"github.com/pkg/errors"
"helm.sh/helm/v3/internal/third_party/dep/fs"
"helm.sh/helm/v3/pkg/helmpath"
"helm.sh/helm/v3/pkg/plugin/cache"
)
@ -43,7 +44,7 @@ func existingVCSRepo(location string) (Installer, error) {
Repo: repo,
base: newBase(repo.Remote()),
}
return i, err
return i, nil
}
// NewVCSInstaller creates a new VCSInstaller.
@ -65,7 +66,7 @@ func NewVCSInstaller(source, version string) (*VCSInstaller, error) {
return i, err
}
// Install clones a remote repository and creates a symlink to the plugin directory.
// Install clones a remote repository and installs into the plugin directory.
//
// Implements Installer.
func (i *VCSInstaller) Install() error {
@ -87,7 +88,8 @@ func (i *VCSInstaller) Install() error {
return ErrMissingMetadata
}
return i.link(i.Repo.LocalPath())
debug("copying %s to %s", i.Repo.LocalPath(), i.Path())
return fs.CopyDir(i.Repo.LocalPath(), i.Path())
}
// Update updates a remote repository

@ -80,24 +80,24 @@ func TestVCSInstaller(t *testing.T) {
t.Fatal(err)
}
if repo.current != "0.1.1" {
t.Errorf("expected version '0.1.1', got %q", repo.current)
t.Fatalf("expected version '0.1.1', got %q", repo.current)
}
if i.Path() != helmpath.DataPath("plugins", "helm-env") {
t.Errorf("expected path '$XDG_CONFIG_HOME/helm/plugins/helm-env', got %q", i.Path())
t.Fatalf("expected path '$XDG_CONFIG_HOME/helm/plugins/helm-env', got %q", i.Path())
}
// Install again to test plugin exists error
if err := Install(i); err == nil {
t.Error("expected error for plugin exists, got none")
t.Fatalf("expected error for plugin exists, got none")
} else if err.Error() != "plugin already exists" {
t.Errorf("expected error for plugin exists, got (%v)", err)
t.Fatalf("expected error for plugin exists, got (%v)", err)
}
// Testing FindSource method, expect error because plugin code is not a cloned repository
if _, err := FindSource(i.Path()); err == nil {
t.Error("expected error for inability to find plugin source, got none")
t.Fatalf("expected error for inability to find plugin source, got none")
} else if err.Error() != "cannot get information about plugin source" {
t.Errorf("expected error for inability to find plugin source, got (%v)", err)
t.Fatalf("expected error for inability to find plugin source, got (%v)", err)
}
}
@ -113,15 +113,14 @@ func TestVCSInstallerNonExistentVersion(t *testing.T) {
}
// ensure a VCSInstaller was returned
_, ok := i.(*VCSInstaller)
if !ok {
if _, ok := i.(*VCSInstaller); !ok {
t.Fatal("expected a VCSInstaller")
}
if err := Install(i); err == nil {
t.Error("expected error for version does not exists, got none")
t.Fatalf("expected error for version does not exists, got none")
} else if err.Error() != fmt.Sprintf("requested version %q does not exist for plugin %q", version, source) {
t.Errorf("expected error for version does not exists, got (%v)", err)
t.Fatalf("expected error for version does not exists, got (%v)", err)
}
}
func TestVCSInstallerUpdate(t *testing.T) {
@ -135,8 +134,7 @@ func TestVCSInstallerUpdate(t *testing.T) {
}
// ensure a VCSInstaller was returned
_, ok := i.(*VCSInstaller)
if !ok {
if _, ok := i.(*VCSInstaller); !ok {
t.Fatal("expected a VCSInstaller")
}
@ -157,7 +155,9 @@ func TestVCSInstallerUpdate(t *testing.T) {
t.Fatal(err)
}
repoRemote := pluginInfo.(*VCSInstaller).Repo.Remote()
vcsInstaller := pluginInfo.(*VCSInstaller)
repoRemote := vcsInstaller.Repo.Remote()
if repoRemote != source {
t.Fatalf("invalid source found, expected %q got %q", source, repoRemote)
}
@ -168,12 +168,14 @@ func TestVCSInstallerUpdate(t *testing.T) {
}
// Test update failure
os.Remove(filepath.Join(i.Path(), "plugin.yaml"))
if err := os.Remove(filepath.Join(vcsInstaller.Repo.LocalPath(), "plugin.yaml")); err != nil {
t.Fatal(err)
}
// Testing update for error
if err := Update(i); err == nil {
t.Error("expected error for plugin modified, got none")
if err := Update(vcsInstaller); err == nil {
t.Fatalf("expected error for plugin modified, got none")
} else if err.Error() != "plugin repo was modified" {
t.Errorf("expected error for plugin modified, got (%v)", err)
t.Fatalf("expected error for plugin modified, got (%v)", err)
}
}

@ -28,14 +28,14 @@ import (
func checkCommand(p *Plugin, extraArgs []string, osStrCmp string, t *testing.T) {
cmd, args, err := p.PrepareCommand(extraArgs)
if err != nil {
t.Errorf(err.Error())
t.Fatal(err)
}
if cmd != "echo" {
t.Errorf("Expected echo, got %q", cmd)
t.Fatalf("Expected echo, got %q", cmd)
}
if l := len(args); l != 5 {
t.Errorf("expected 5 args, got %d", l)
t.Fatalf("expected 5 args, got %d", l)
}
expect := []string{"-n", osStrCmp, "--debug", "--foo", "bar"}
@ -49,13 +49,13 @@ func checkCommand(p *Plugin, extraArgs []string, osStrCmp string, t *testing.T)
p.Metadata.IgnoreFlags = true
cmd, args, err = p.PrepareCommand(extraArgs)
if err != nil {
t.Errorf(err.Error())
t.Fatal(err)
}
if cmd != "echo" {
t.Errorf("Expected echo, got %q", cmd)
t.Fatalf("Expected echo, got %q", cmd)
}
if l := len(args); l != 2 {
t.Errorf("expected 2 args, got %d", l)
t.Fatalf("expected 2 args, got %d", l)
}
expect = []string{"-n", osStrCmp}
for i := 0; i < len(args); i++ {
@ -155,7 +155,7 @@ func TestNoPrepareCommand(t *testing.T) {
_, _, err := p.PrepareCommand(argv)
if err == nil {
t.Errorf("Expected error to be returned")
t.Fatalf("Expected error to be returned")
}
}
@ -172,7 +172,7 @@ func TestNoMatchPrepareCommand(t *testing.T) {
argv := []string{"--debug", "--foo", "bar"}
if _, _, err := p.PrepareCommand(argv); err == nil {
t.Errorf("Expected error to be returned")
t.Fatalf("Expected error to be returned")
}
}
@ -184,7 +184,7 @@ func TestLoadDir(t *testing.T) {
}
if plug.Dir != dirname {
t.Errorf("Expected dir %q, got %q", dirname, plug.Dir)
t.Fatalf("Expected dir %q, got %q", dirname, plug.Dir)
}
expect := &Metadata{
@ -200,7 +200,7 @@ func TestLoadDir(t *testing.T) {
}
if !reflect.DeepEqual(expect, plug.Metadata) {
t.Errorf("Expected plugin metadata %v, got %v", expect, plug.Metadata)
t.Fatalf("Expected plugin metadata %v, got %v", expect, plug.Metadata)
}
}
@ -212,7 +212,7 @@ func TestDownloader(t *testing.T) {
}
if plug.Dir != dirname {
t.Errorf("Expected dir %q, got %q", dirname, plug.Dir)
t.Fatalf("Expected dir %q, got %q", dirname, plug.Dir)
}
expect := &Metadata{
@ -230,7 +230,7 @@ func TestDownloader(t *testing.T) {
}
if !reflect.DeepEqual(expect, plug.Metadata) {
t.Errorf("Expected metadata %v, got %v", expect, plug.Metadata)
t.Fatalf("Expected metadata %v, got %v", expect, plug.Metadata)
}
}

Loading…
Cancel
Save