include external path

Signed-off-by: Marta Rubelj <marta.rubelj@true-north.hr>
pull/10674/head
Marta Rubelj 4 years ago
parent 12f1bc0acd
commit faa99831cf

@ -63,6 +63,11 @@ func addChartPathOptionsFlags(f *pflag.FlagSet, c *action.ChartPathOptions) {
f.BoolVar(&c.PassCredentialsAll, "pass-credentials", false, "pass credentials to all domains") f.BoolVar(&c.PassCredentialsAll, "pass-credentials", false, "pass credentials to all domains")
} }
// addExternalPathsFlags adds flags to the given command
func addExternalPathsFlags(f *pflag.FlagSet, v *[]string) {
f.StringArrayVar(v, "include-path", []string{}, "paths to local directories to add during chart installation")
}
// bindOutputFlag will add the output flag to the given command and bind the // bindOutputFlag will add the output flag to the given command and bind the
// value to the given format pointer // value to the given format pointer
func bindOutputFlag(cmd *cobra.Command, varRef *output.Format) { func bindOutputFlag(cmd *cobra.Command, varRef *output.Format) {

@ -80,6 +80,11 @@ func main() {
} }
}) })
if err := actionConfig.Init(settings.RESTClientGetter(), settings.Namespace(), os.Getenv("HELM_DRIVER"), debug); err != nil {
debug("%+v", err)
os.Exit(1)
}
if err := cmd.Execute(); err != nil { if err := cmd.Execute(); err != nil {
debug("%+v", err) debug("%+v", err)
switch e := err.(type) { switch e := err.(type) {

@ -157,6 +157,7 @@ func addInstallFlags(cmd *cobra.Command, f *pflag.FlagSet, client *action.Instal
f.BoolVar(&client.SubNotes, "render-subchart-notes", false, "if set, render subchart notes along with the parent") f.BoolVar(&client.SubNotes, "render-subchart-notes", false, "if set, render subchart notes along with the parent")
addValueOptionsFlags(f, valueOpts) addValueOptionsFlags(f, valueOpts)
addChartPathOptionsFlags(f, &client.ChartPathOptions) addChartPathOptionsFlags(f, &client.ChartPathOptions)
addExternalPathsFlags(f, &client.ExternalPaths)
err := cmd.RegisterFlagCompletionFunc("version", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { err := cmd.RegisterFlagCompletionFunc("version", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
requiredArgs := 2 requiredArgs := 2

@ -77,6 +77,18 @@ func TestInstall(t *testing.T) {
cmd: "install virgil testdata/testcharts/alpine -f testdata/testcharts/alpine/extra_values.yaml", cmd: "install virgil testdata/testcharts/alpine -f testdata/testcharts/alpine/extra_values.yaml",
golden: "output/install-with-values-file.txt", golden: "output/install-with-values-file.txt",
}, },
// Install, external dir
{
name: "install with external dir",
cmd: "install virgil testdata/testcharts/external --set glob.enabled=true --include-path testdata/files/",
golden: "output/install-with-external-files.txt",
},
// Install, external dir with alias
{
name: "chart with template with external dir and alias",
cmd: "install virgil testdata/testcharts/externalAlias --set glob.enabled=true --include-path vol1@testdata/files/",
golden: "output/install-with-external-files.txt",
},
// Install, no hooks // Install, no hooks
{ {
name: "install without hooks", name: "install without hooks",

@ -131,6 +131,16 @@ func TestTemplateCmd(t *testing.T) {
cmd: fmt.Sprintf(`template '%s' --skip-tests`, chartPath), cmd: fmt.Sprintf(`template '%s' --skip-tests`, chartPath),
golden: "output/template-skip-tests.txt", golden: "output/template-skip-tests.txt",
}, },
{
name: "chart with template with external dir",
cmd: fmt.Sprintf("template '%s' --set glob.enabled=true --include-path testdata/files/", "testdata/testcharts/external"),
golden: "output/template-with-external-dir.txt",
},
{
name: "chart with template with external dir and alias",
cmd: fmt.Sprintf("template '%s' --set glob.enabled=true --include-path vol1@testdata/files/", "testdata/testcharts/externalAlias"),
golden: "output/template-with-external-dir-and-alias.txt",
},
} }
runTestCmd(t, tests) runTestCmd(t, tests)
} }

@ -0,0 +1 @@
out-of-chart-dir

@ -0,0 +1,6 @@
NAME: virgil
LAST DEPLOYED: Fri Sep 2 22:04:05 1977
NAMESPACE: default
STATUS: deployed
REVISION: 1
TEST SUITE: None

@ -0,0 +1,25 @@
---
# Source: configmap/templates/config-map.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: "release-name-"
labels:
# The "app.kubernetes.io/managed-by" label is used to track which tool
# deployed a given chart. It is useful for admins who want to see what
# releases a particular tool is responsible for.
app.kubernetes.io/managed-by: "Helm"
# The "app.kubernetes.io/instance" convention makes it easy to tie a release
# to all of the Kubernetes resources that were created as part of that
# release.
app.kubernetes.io/instance: "release-name"
app.kubernetes.io/version: 1.0
# This makes it easy to audit chart usage.
helm.sh/chart: "configmap-0.1.0"
data:
external.1.conf: |
external-1
external.2.conf: |
external-2
external.txt: |
out-of-chart-dir

@ -0,0 +1,25 @@
---
# Source: configmap/templates/config-map.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: "release-name-"
labels:
# The "app.kubernetes.io/managed-by" label is used to track which tool
# deployed a given chart. It is useful for admins who want to see what
# releases a particular tool is responsible for.
app.kubernetes.io/managed-by: "Helm"
# The "app.kubernetes.io/instance" convention makes it easy to tie a release
# to all of the Kubernetes resources that were created as part of that
# release.
app.kubernetes.io/instance: "release-name"
app.kubernetes.io/version: 1.0
# This makes it easy to audit chart usage.
helm.sh/chart: "configmap-0.1.0"
data:
external.1.conf: |
external-1
external.2.conf: |
external-2
external.txt: |
out-of-chart-dir

@ -0,0 +1,21 @@
---
# Source: configmap/templates/config-map.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: "RELEASE-NAME-"
labels:
# The "app.kubernetes.io/managed-by" label is used to track which tool
# deployed a given chart. It is useful for admins who want to see what
# releases a particular tool is responsible for.
app.kubernetes.io/managed-by: "Helm"
# The "app.kubernetes.io/instance" convention makes it easy to tie a release
# to all of the Kubernetes resources that were created as part of that
# release.
app.kubernetes.io/instance: "RELEASE-NAME"
app.kubernetes.io/version: 1.0
# This makes it easy to audit chart usage.
helm.sh/chart: "configmap-0.1.0"
data:
external.txt: |
out-of-chart-dir

@ -0,0 +1,8 @@
apiVersion: v1
appVersion: "1.0"
description: Deploy a basic Config Map from an external file
home: https://helm.sh/helm
name: configmap
sources:
- https://github.com/helm/helm
version: 0.1.0

@ -0,0 +1,23 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: "{{.Release.Name}}-{{.Values.Name}}"
labels:
# The "app.kubernetes.io/managed-by" label is used to track which tool
# deployed a given chart. It is useful for admins who want to see what
# releases a particular tool is responsible for.
app.kubernetes.io/managed-by: {{.Release.Service | quote }}
# The "app.kubernetes.io/instance" convention makes it easy to tie a release
# to all of the Kubernetes resources that were created as part of that
# release.
app.kubernetes.io/instance: {{.Release.Name | quote }}
app.kubernetes.io/version: {{ .Chart.AppVersion }}
# This makes it easy to audit chart usage.
helm.sh/chart: "{{.Chart.Name}}-{{.Chart.Version}}"
data:
{{- if .Values.external }}
{{ (.Files.Glob .Values.external).AsConfig | indent 2 }}
{{- end }}
{{- if .Values.glob.enabled }}
{{ (.Files.Glob .Values.glob.path).AsConfig | indent 2 }}
{{- end }}

@ -0,0 +1,4 @@
external: false
glob:
enabled: false
path: "*"

@ -0,0 +1,8 @@
apiVersion: v1
appVersion: "1.0"
description: Deploy a basic Config Map from an external file
home: https://helm.sh/helm
name: configmap
sources:
- https://github.com/helm/helm
version: 0.1.0

@ -0,0 +1,23 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: "{{.Release.Name}}-{{.Values.Name}}"
labels:
# The "app.kubernetes.io/managed-by" label is used to track which tool
# deployed a given chart. It is useful for admins who want to see what
# releases a particular tool is responsible for.
app.kubernetes.io/managed-by: {{.Release.Service | quote }}
# The "app.kubernetes.io/instance" convention makes it easy to tie a release
# to all of the Kubernetes resources that were created as part of that
# release.
app.kubernetes.io/instance: {{.Release.Name | quote }}
app.kubernetes.io/version: {{ .Chart.AppVersion }}
# This makes it easy to audit chart usage.
helm.sh/chart: "{{.Chart.Name}}-{{.Chart.Version}}"
data:
{{- if .Values.external }}
{{ (.Files.Glob .Values.external).AsConfig | indent 2 }}
{{- end }}
{{- if .Values.glob.enabled }}
{{ (.Files.Glob .Values.glob.path).AsConfig | indent 2 }}
{{- end }}

@ -0,0 +1,4 @@
external: false
glob:
enabled: false
path: "vol1/*"

@ -115,6 +115,7 @@ func newUpgradeCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
instClient.PostRenderer = client.PostRenderer instClient.PostRenderer = client.PostRenderer
instClient.DisableOpenAPIValidation = client.DisableOpenAPIValidation instClient.DisableOpenAPIValidation = client.DisableOpenAPIValidation
instClient.SubNotes = client.SubNotes instClient.SubNotes = client.SubNotes
instClient.ExternalPaths = client.ExternalPaths
instClient.Description = client.Description instClient.Description = client.Description
rel, err := runInstall(args, instClient, valueOpts, out) rel, err := runInstall(args, instClient, valueOpts, out)
@ -233,6 +234,7 @@ func newUpgradeCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
addValueOptionsFlags(f, valueOpts) addValueOptionsFlags(f, valueOpts)
bindOutputFlag(cmd, &outfmt) bindOutputFlag(cmd, &outfmt)
bindPostRenderFlag(cmd, &client.PostRenderer) bindPostRenderFlag(cmd, &client.PostRenderer)
addExternalPathsFlags(f, &client.ExternalPaths)
err := cmd.RegisterFlagCompletionFunc("version", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { err := cmd.RegisterFlagCompletionFunc("version", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
if len(args) != 2 { if len(args) != 2 {

@ -357,6 +357,38 @@ func TestUpgradeInstallWithValuesFromStdin(t *testing.T) {
} }
func TestUpgradeWithExternalFile(t *testing.T) {
releaseName := "funny-bunny-v7"
exFiles := []*chart.File{
{Name: "testdata/files/external.txt", Data: []byte("from-external-file")},
}
relMock, ch, chartPath := prepareMockReleaseWithExternal(releaseName, exFiles, t)
defer resetEnv()()
store := storageFixture()
store.Create(relMock(releaseName, 3, ch))
cmd := fmt.Sprintf("upgrade %s --set glob.enabled=false --set external=testdata/files/external.txt '%s'", releaseName, chartPath)
_, _, err := executeActionCommandC(store, cmd)
if err != nil {
t.Errorf("unexpected error, got '%v'", err)
}
updatedRel, err := store.Get(releaseName, 4)
if err != nil {
t.Errorf("unexpected error, got '%v'", err)
}
if !strings.Contains(updatedRel.Manifest, "from-external-file") {
t.Errorf("The value is not set correctly. manifest: %s", updatedRel.Manifest)
}
}
func prepareMockRelease(releaseName string, t *testing.T) (func(n string, v int, ch *chart.Chart) *release.Release, *chart.Chart, string) { func prepareMockRelease(releaseName string, t *testing.T) (func(n string, v int, ch *chart.Chart) *release.Release, *chart.Chart, string) {
tmpChart := ensure.TempDir(t) tmpChart := ensure.TempDir(t)
configmapData, err := ioutil.ReadFile("testdata/testcharts/upgradetest/templates/configmap.yaml") configmapData, err := ioutil.ReadFile("testdata/testcharts/upgradetest/templates/configmap.yaml")
@ -392,6 +424,43 @@ func prepareMockRelease(releaseName string, t *testing.T) (func(n string, v int,
return relMock, ch, chartPath return relMock, ch, chartPath
} }
func prepareMockReleaseWithExternal(releaseName string, exFiles []*chart.File, t *testing.T) (func(n string, v int, ch *chart.Chart) *release.Release, *chart.Chart, string) {
tmpChart := ensure.TempDir(t)
configmapData, err := ioutil.ReadFile("testdata/testcharts/external/templates/config-map.yaml")
if err != nil {
t.Fatalf("Error loading template yaml %v", err)
}
cfile := &chart.Chart{
Metadata: &chart.Metadata{
APIVersion: chart.APIVersionV1,
Name: "testUpgradeChart",
Description: "A Helm chart for Kubernetes",
Version: "0.1.0",
},
Templates: []*chart.File{{Name: "templates/configmap.yaml", Data: configmapData}},
Files: exFiles,
}
chartPath := filepath.Join(tmpChart, cfile.Metadata.Name)
if err := chartutil.SaveDir(cfile, tmpChart); err != nil {
t.Fatalf("Error creating chart for upgrade: %v", err)
}
ch, err := loader.Load(chartPath)
if err != nil {
t.Fatalf("Error loading chart: %v", err)
}
_ = release.Mock(&release.MockReleaseOptions{
Name: releaseName,
Chart: ch,
})
relMock := func(n string, v int, ch *chart.Chart) *release.Release {
return release.Mock(&release.MockReleaseOptions{Name: n, Version: v, Chart: ch})
}
return relMock, ch, chartPath
}
func TestUpgradeOutputCompletion(t *testing.T) { func TestUpgradeOutputCompletion(t *testing.T) {
outputFlagCompletionTest(t, "upgrade") outputFlagCompletionTest(t, "upgrade")
} }

@ -88,7 +88,7 @@ func compare(actual []byte, filename string) error {
} }
expected = normalize(expected) expected = normalize(expected)
if !bytes.Equal(expected, actual) { if !bytes.Equal(expected, actual) {
return errors.Errorf("does not match golden file %s\n\nWANT:\n'%s'\n\nGOT:\n'%s'\n", filename, expected, actual) return errors.Errorf("does not match golden file %s\n\nWANT:\n'%s'\n\nGOT:\n'%s'", filename, expected, actual)
} }
return nil return nil
} }

@ -17,6 +17,7 @@ package action
import ( import (
"flag" "flag"
"fmt"
"io/ioutil" "io/ioutil"
"os" "os"
"testing" "testing"
@ -217,6 +218,15 @@ func withSampleIncludingIncorrectTemplates() chartOption {
} }
} }
func withExternalFileTemplate(externalPath string) chartOption {
return func(opts *chartOptions) {
externalFilesTemplates := []*chart.File{
{Name: "templates/with-external-paths", Data: []byte(fmt.Sprintf(`data: {{ .Files.Get "%s" }}`, externalPath))},
}
opts.Templates = append(opts.Templates, externalFilesTemplates...)
}
}
func withMultipleManifestTemplate() chartOption { func withMultipleManifestTemplate() chartOption {
return func(opts *chartOptions) { return func(opts *chartOptions) {
sampleTemplates := []*chart.File{ sampleTemplates := []*chart.File{

@ -25,11 +25,14 @@ import (
"os" "os"
"path" "path"
"path/filepath" "path/filepath"
"strconv"
"strings" "strings"
"sync" "sync"
"text/template" "text/template"
"time" "time"
"helm.sh/helm/v3/pkg/chart/loader"
"github.com/Masterminds/sprig/v3" "github.com/Masterminds/sprig/v3"
"github.com/pkg/errors" "github.com/pkg/errors"
v1 "k8s.io/api/core/v1" v1 "k8s.io/api/core/v1"
@ -102,6 +105,8 @@ type Install struct {
PostRenderer postrender.PostRenderer PostRenderer postrender.PostRenderer
// Lock to control raceconditions when the process receives a SIGTERM // Lock to control raceconditions when the process receives a SIGTERM
Lock sync.Mutex Lock sync.Mutex
// ExternalPaths is list of included files in configuration
ExternalPaths []string
} }
// ChartPathOptions captures common options used for controlling chart paths // ChartPathOptions captures common options used for controlling chart paths
@ -176,10 +181,76 @@ func (i *Install) installCRDs(crds []chart.CRD) error {
return nil return nil
} }
// loadExternalPaths takes external paths from a given configuration and puts the files in a chart.
// The alias can be defined, but if not, the alias will be the ordinal number of the path in the list starting from 0.
// Example: install test ./bin/test --include-path vol_name1@/path/to/dir1 --include-path /path/to/dir2
// The first included file has the alias vol_name1, and it can be used from the config map: {{ (.Files.Glob "vol1/**").AsConfig | nindent 2 }}
// The Second included file hasn't an alias but is generated with the number 1, and it can be used from the config map {{ (.Files.Glob "1/**").AsConfig | nindent 2 }}
func loadExternalPaths(ch *chart.Chart, externalPaths []string) error {
var errs []string
includeDefaultAlias := false
if len(externalPaths) > 1 {
includeDefaultAlias = true
}
for i, p := range externalPaths {
var alias string
if strings.Contains(p, "@") {
aliasPath := strings.Split(p, "@")
alias = aliasPath[0]
p = aliasPath[1]
}
allPaths, err := loader.ExpandFilePath(p)
if err != nil {
errs = append(errs, fmt.Sprintf("%s (path not accessible)", p))
}
for _, currentPath := range allPaths {
fileContentBytes, err := ioutil.ReadFile(currentPath)
if err != nil {
errs = append(errs, fmt.Sprintf("%s (not readable)", currentPath))
continue
}
p = strings.ReplaceAll(p, "\\", "/")
if !strings.HasSuffix(p, "/") {
p = p + "/"
}
if currentPath == p {
errs = append(errs, fmt.Sprintf("%s (accepts only directory, not file)", currentPath))
}
if includeDefaultAlias {
if alias != "" {
currentPath = strings.Replace(currentPath, p, alias+"/", 1)
} else {
currentPath = strings.Replace(currentPath, p, strconv.Itoa(i)+"/", 1)
}
} else {
if alias != "" {
currentPath = strings.Replace(currentPath, p, alias+"/", 1)
} else {
currentPath = strings.Replace(currentPath, p, "", 1)
}
}
newFile := chart.File{Name: currentPath, Data: fileContentBytes}
for _, file := range ch.Files {
if file.Name == newFile.Name && bytes.Equal(file.Data, newFile.Data) {
continue
}
}
ch.Files = append(ch.Files, &newFile)
}
}
if len(errs) > 0 {
return errors.New(fmt.Sprint("Failed to load external paths: ", strings.Join(errs, "; ")))
}
return nil
}
// Run executes the installation // Run executes the installation
// //
// If DryRun is set to true, this will prepare the release, but not install it // If DryRun is set to true, this will prepare the release, but not install it
func (i *Install) Run(chrt *chart.Chart, vals map[string]interface{}) (*release.Release, error) { func (i *Install) Run(chrt *chart.Chart, vals map[string]interface{}) (*release.Release, error) {
ctx := context.Background() ctx := context.Background()
return i.RunWithContext(ctx, chrt, vals) return i.RunWithContext(ctx, chrt, vals)
@ -187,6 +258,9 @@ func (i *Install) Run(chrt *chart.Chart, vals map[string]interface{}) (*release.
// Run executes the installation with Context // Run executes the installation with Context
func (i *Install) RunWithContext(ctx context.Context, chrt *chart.Chart, vals map[string]interface{}) (*release.Release, error) { func (i *Install) RunWithContext(ctx context.Context, chrt *chart.Chart, vals map[string]interface{}) (*release.Release, error) {
if err := loadExternalPaths(chrt, i.ExternalPaths); err != nil {
return nil, err
}
// Check reachability of cluster unless in client-only mode (e.g. `helm template` without `--validate`) // Check reachability of cluster unless in client-only mode (e.g. `helm template` without `--validate`)
if !i.ClientOnly { if !i.ClientOnly {
if err := i.cfg.KubeClient.IsReachable(); err != nil { if err := i.cfg.KubeClient.IsReachable(); err != nil {

@ -40,12 +40,21 @@ import (
helmtime "helm.sh/helm/v3/pkg/time" helmtime "helm.sh/helm/v3/pkg/time"
) )
const ExternalFileRelPath = "testdata/files"
const ExternalFileName = "external.txt"
type nameTemplateTestCase struct { type nameTemplateTestCase struct {
tpl string tpl string
expected string expected string
expectedErrorStr string expectedErrorStr string
} }
type includeExternalPathTestCase struct {
Name string
IncludedFilePath string
ExternalPath string
}
func installAction(t *testing.T) *Install { func installAction(t *testing.T) *Install {
config := actionConfigFixture(t) config := actionConfigFixture(t)
instAction := NewInstall(config) instAction := NewInstall(config)
@ -726,3 +735,51 @@ func TestNameAndChartGenerateName(t *testing.T) {
}) })
} }
} }
func TestInstallFailsWhenWrongPathsIncluded(t *testing.T) {
is := assert.New(t)
vals := map[string]interface{}{}
tests := []includeExternalPathTestCase{
{
Name: "included paths not passed",
IncludedFilePath: "",
ExternalPath: ExternalFileRelPath,
},
}
for _, tc := range tests {
t.Run(tc.Name, func(t *testing.T) {
instAction := installAction(t)
instAction.ExternalPaths = append(instAction.ExternalPaths, tc.IncludedFilePath)
_, err := instAction.Run(buildChart(withExternalFileTemplate(tc.ExternalPath)), vals)
expectedErr := fmt.Sprintf("<.Files.Get>: error calling Get: file %s not included", tc.ExternalPath)
is.Error(err, expectedErr)
})
}
}
func TestInstallWhenIncludePathsPassed(t *testing.T) {
is := assert.New(t)
vals := map[string]interface{}{}
tests := []includeExternalPathTestCase{
{
Name: "relative path is included and external file is relative",
IncludedFilePath: ExternalFileRelPath,
ExternalPath: ExternalFileName,
},
}
for _, tc := range tests {
t.Run(tc.Name, func(t *testing.T) {
instAction := installAction(t)
instAction.ExternalPaths = append(instAction.ExternalPaths, tc.IncludedFilePath)
installRelease, err := instAction.Run(buildChart(withExternalFileTemplate(tc.ExternalPath)), vals)
is.Contains(installRelease.Manifest, "out-of-chart-dir")
is.NoError(err)
})
}
}

@ -0,0 +1,63 @@
/*
Copyright The Helm Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package action
import (
"testing"
"github.com/stretchr/testify/assert"
)
func rollbackAction(t *testing.T) *Rollback {
config := actionConfigFixture(t)
rollbackAction := NewRollback(config)
return rollbackAction
}
func TestRollbackToReleaseWithExternalFile(t *testing.T) {
is := assert.New(t)
vals := map[string]interface{}{}
chartVersion1 := buildChart(withExternalFileTemplate(ExternalFileName))
chartVersion2 := buildChart()
instAction := installAction(t)
instAction.ExternalPaths = append(instAction.ExternalPaths, ExternalFileRelPath)
relVersion1, err := instAction.Run(chartVersion1, vals)
is.Contains(relVersion1.Manifest, "out-of-chart-dir")
is.NoError(err)
upAction := upgradeAction(t)
err = upAction.cfg.Releases.Create(relVersion1)
is.NoError(err)
relVersion2, err := upAction.Run(relVersion1.Name, chartVersion2, vals)
is.NotContains(relVersion2.Manifest, "out-out-chart-dir")
is.NoError(err)
rollAction := rollbackAction(t)
err = rollAction.cfg.Releases.Create(relVersion1)
is.NoError(err)
err = rollAction.cfg.Releases.Create(relVersion2)
is.NoError(err)
currentRelease, targetRelease, err := rollAction.prepareRollback(relVersion2.Name)
is.NoError(err)
relVersion3, err := rollAction.performRollback(currentRelease, targetRelease)
is.NoError(err)
is.Contains(relVersion3.Manifest, "out-of-chart-dir")
}

@ -0,0 +1 @@
out-of-chart-dir

@ -103,6 +103,8 @@ type Upgrade struct {
DependencyUpdate bool DependencyUpdate bool
// Lock to control race conditions when the process receives a SIGTERM // Lock to control race conditions when the process receives a SIGTERM
Lock sync.Mutex Lock sync.Mutex
// ExternalPaths is list of included files in configuration
ExternalPaths []string
} }
type resultMessage struct { type resultMessage struct {
@ -179,6 +181,10 @@ func (u *Upgrade) prepareUpgrade(name string, chart *chart.Chart, vals map[strin
return nil, nil, err return nil, nil, err
} }
if err := loadExternalPaths(chart, u.ExternalPaths); err != nil {
return nil, nil, err
}
// Concurrent `helm upgrade`s will either fail here with `errPending` or when creating the release with "already exists". This should act as a pessimistic lock. // Concurrent `helm upgrade`s will either fail here with `errPending` or when creating the release with "already exists". This should act as a pessimistic lock.
if lastRelease.Info.Status.IsPending() { if lastRelease.Info.Status.IsPending() {
return nil, nil, errPending return nil, nil, errPending

@ -326,6 +326,87 @@ func TestUpgradeRelease_Pending(t *testing.T) {
req.Contains(err.Error(), "progress", err) req.Contains(err.Error(), "progress", err)
} }
func TestUpgradeFailsWhenEmptyPathIncluded(t *testing.T) {
is := assert.New(t)
vals := map[string]interface{}{}
t.Run("included paths not passed", func(t *testing.T) {
upAction := upgradeAction(t)
upAction.ExternalPaths = append(upAction.ExternalPaths, "")
rel := releaseStub()
rel.Name = "test"
rel.Info.Status = release.StatusDeployed
upAction.cfg.Releases.Create(rel)
_, err := upAction.Run(rel.Name, buildChart(withExternalFileTemplate("external.txt")), vals)
expectedErr := "Failed to load external paths: (path not accessible)"
is.Error(err, expectedErr)
})
}
func TestUpgradeFailsWhenWrongPathsIncluded(t *testing.T) {
is := assert.New(t)
vals := map[string]interface{}{}
tests := []includeExternalPathTestCase{
{
Name: "included paths not passed",
IncludedFilePath: "testdata/files2",
ExternalPath: "external.txt",
},
{
Name: "included paths not passed",
IncludedFilePath: "testdata2/files",
ExternalPath: "external.txt",
},
}
for _, tc := range tests {
t.Run(tc.Name, func(t *testing.T) {
upAction := upgradeAction(t)
upAction.ExternalPaths = append(upAction.ExternalPaths, tc.IncludedFilePath)
rel := releaseStub()
rel.Name = "test"
rel.Info.Status = release.StatusDeployed
upAction.cfg.Releases.Create(rel)
_, err := upAction.Run(rel.Name, buildChart(withExternalFileTemplate(tc.ExternalPath)), vals)
expectedErr := fmt.Sprintf("Failed to load external paths:%s(path not accessible)", tc.IncludedFilePath)
is.Error(err, expectedErr)
})
}
}
func TestUpgradeWhenIncludePathsPassed(t *testing.T) {
is := assert.New(t)
vals := map[string]interface{}{}
tests := []includeExternalPathTestCase{
{
Name: "relative path of directory is included and external file is relative",
IncludedFilePath: "testdata/files",
ExternalPath: "external.txt",
},
}
for _, tc := range tests {
t.Run(tc.Name, func(t *testing.T) {
upAction := upgradeAction(t)
upAction.ExternalPaths = append(upAction.ExternalPaths, tc.IncludedFilePath)
rel := releaseStub()
rel.Name = "test"
rel.Info.Status = release.StatusDeployed
upAction.cfg.Releases.Create(rel)
upgradeRelease, err := upAction.Run(rel.Name, buildChart(withExternalFileTemplate(tc.ExternalPath)), vals)
is.Contains(upgradeRelease.Manifest, "out-of-chart-dir")
is.NoError(err)
})
}
}
func TestUpgradeRelease_Interrupted_Wait(t *testing.T) { func TestUpgradeRelease_Interrupted_Wait(t *testing.T) {
is := assert.New(t) is := assert.New(t)

@ -0,0 +1,38 @@
/*
Copyright The Helm Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package loader
import (
"io/fs"
"os"
"path/filepath"
)
// ExpandFilePath expands a local file, dir or glob path to a list of files
func ExpandFilePath(path string) ([]string, error) {
_, err := os.Stat(path)
if err != nil {
return nil, err
}
var files []string
filepath.WalkDir(path, func(path string, d fs.DirEntry, err error) error {
if !d.IsDir() {
files = append(files, filepath.ToSlash(path))
}
return nil
})
return files, nil
}

@ -0,0 +1,33 @@
/*
Copyright The Helm Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package loader
import (
"testing"
"github.com/stretchr/testify/require"
)
func TestExpandFilePath(t *testing.T) {
req := require.New(t)
dir, err := ExpandFilePath("testdata/albatross/")
req.NoError(err)
req.Contains(dir, "testdata/albatross/Chart.yaml")
req.Contains(dir, "testdata/albatross/values.yaml")
file, err := ExpandFilePath("testdata/albatross/Chart.yaml")
req.NoError(err)
req.Contains(file, "testdata/albatross/Chart.yaml")
}

@ -18,6 +18,7 @@ package engine
import ( import (
"encoding/base64" "encoding/base64"
"fmt"
"path" "path"
"strings" "strings"
@ -50,6 +51,7 @@ func (f files) GetBytes(name string) []byte {
if v, ok := f[name]; ok { if v, ok := f[name]; ok {
return v return v
} }
fmt.Printf("file %s not included", name)
return []byte{} return []byte{}
} }
@ -60,7 +62,8 @@ func (f files) GetBytes(name string) []byte {
// //
// {{.Files.Get "foo"}} // {{.Files.Get "foo"}}
func (f files) Get(name string) string { func (f files) Get(name string) string {
return string(f.GetBytes(name)) content := f.GetBytes(name)
return string(content)
} }
// Glob takes a glob pattern and returns another files object only containing // Glob takes a glob pattern and returns another files object only containing
@ -102,7 +105,8 @@ func (f files) Glob(pattern string) files {
// data: // data:
// {{ .Files.Glob("config/**").AsConfig() | indent 4 }} // {{ .Files.Glob("config/**").AsConfig() | indent 4 }}
func (f files) AsConfig() string { func (f files) AsConfig() string {
if f == nil { if f == nil || len(f) == 0 {
fmt.Println("must pass path")
return "" return ""
} }
@ -131,7 +135,8 @@ func (f files) AsConfig() string {
// data: // data:
// {{ .Files.Glob("secrets/*").AsSecrets() }} // {{ .Files.Glob("secrets/*").AsSecrets() }}
func (f files) AsSecrets() string { func (f files) AsSecrets() string {
if f == nil { if f == nil || len(f) == 0 {
fmt.Println("must pass files")
return "" return ""
} }
@ -153,7 +158,8 @@ func (f files) AsSecrets() string {
// {{ . }}{{ end }} // {{ . }}{{ end }}
func (f files) Lines(path string) []string { func (f files) Lines(path string) []string {
if f == nil || f[path] == nil { if f == nil || f[path] == nil {
return []string{} fmt.Println("must pass files")
return nil
} }
return strings.Split(string(f[path]), "\n") return strings.Split(string(f[path]), "\n")

@ -21,6 +21,8 @@ import (
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
const NonExistingFileName = "no_such_file.txt"
var cases = []struct { var cases = []struct {
path, data string path, data string
}{ }{
@ -49,12 +51,22 @@ func TestNewFiles(t *testing.T) {
if got := string(files.GetBytes(f.path)); got != f.data { if got := string(files.GetBytes(f.path)); got != f.data {
t.Errorf("%d: expected %q, got %q", i, f.data, got) t.Errorf("%d: expected %q, got %q", i, f.data, got)
} }
if got := files.Get(f.path); got != f.data { if got := files.Get(f.path); got != f.data {
t.Errorf("%d: expected %q, got %q", i, f.data, got) t.Errorf("%d: expected %q, got %q", i, f.data, got)
} }
} }
} }
func TestGetNonExistingFile(t *testing.T) {
as := assert.New(t)
f := getTestFiles()
content := f.Get(NonExistingFileName)
as.Empty(content)
}
func TestFileGlob(t *testing.T) { func TestFileGlob(t *testing.T) {
as := assert.New(t) as := assert.New(t)
@ -63,6 +75,7 @@ func TestFileGlob(t *testing.T) {
matched := f.Glob("story/**") matched := f.Glob("story/**")
as.Len(matched, 2, "Should be two files in glob story/**") as.Len(matched, 2, "Should be two files in glob story/**")
as.Equal("Joseph Conrad", matched.Get("story/author.txt")) as.Equal("Joseph Conrad", matched.Get("story/author.txt"))
} }
@ -75,6 +88,9 @@ func TestToConfig(t *testing.T) {
out = f.Glob("ship/**").AsConfig() out = f.Glob("ship/**").AsConfig()
as.Equal("captain.txt: The Captain\nstowaway.txt: Legatt", out) as.Equal("captain.txt: The Captain\nstowaway.txt: Legatt", out)
out = f.Glob(NonExistingFileName).AsConfig()
as.Empty(out)
} }
func TestToSecret(t *testing.T) { func TestToSecret(t *testing.T) {
@ -84,6 +100,9 @@ func TestToSecret(t *testing.T) {
out := f.Glob("ship/**").AsSecrets() out := f.Glob("ship/**").AsSecrets()
as.Equal("captain.txt: VGhlIENhcHRhaW4=\nstowaway.txt: TGVnYXR0", out) as.Equal("captain.txt: VGhlIENhcHRhaW4=\nstowaway.txt: TGVnYXR0", out)
out = f.Glob(NonExistingFileName).AsSecrets()
as.Empty(out)
} }
func TestLines(t *testing.T) { func TestLines(t *testing.T) {
@ -93,6 +112,8 @@ func TestLines(t *testing.T) {
out := f.Lines("multiline/test.txt") out := f.Lines("multiline/test.txt")
as.Len(out, 2) as.Len(out, 2)
as.Equal("bar", out[0]) as.Equal("bar", out[0])
out = f.Lines(NonExistingFileName)
as.Nil(out)
} }

Loading…
Cancel
Save