add include file

Signed-off-by: Matheus Hunsche <matheus.hunsche@ifood.com.br>
pull/10077/head
Matheus Hunsche 5 years ago committed by itaispiegel
parent 1819f1f616
commit f9d8645c17

@ -29,6 +29,7 @@ import (
"k8s.io/klog/v2"
"helm.sh/helm/v3/pkg/action"
"helm.sh/helm/v3/pkg/cli/files"
"helm.sh/helm/v3/pkg/cli/output"
"helm.sh/helm/v3/pkg/cli/values"
"helm.sh/helm/v3/pkg/helmpath"
@ -64,6 +65,11 @@ func addChartPathOptionsFlags(f *pflag.FlagSet, c *action.ChartPathOptions) {
f.BoolVar(&c.PassCredentialsAll, "pass-credentials", false, "pass credentials to all domains")
}
func addExternalFilesFlags(f *pflag.FlagSet, v *files.ExternalFiles) {
f.StringArrayVar(&v.Files, "include-file", []string{}, "paths to local files to add during chart installation")
f.StringArrayVar(&v.Globs, "include-dir", []string{}, "paths or globs to local dirs to add during chart installation")
}
// bindOutputFlag will add the output flag to the given command and bind the
// value to the given format pointer
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 {
debug("%+v", err)
switch e := err.(type) {

@ -20,9 +20,11 @@ import (
"context"
"fmt"
"io"
"io/ioutil"
"log"
"os"
"os/signal"
"strings"
"syscall"
"time"
@ -34,6 +36,7 @@ import (
"helm.sh/helm/v3/pkg/action"
"helm.sh/helm/v3/pkg/chart"
"helm.sh/helm/v3/pkg/chart/loader"
"helm.sh/helm/v3/pkg/cli/files"
"helm.sh/helm/v3/pkg/cli/output"
"helm.sh/helm/v3/pkg/cli/values"
"helm.sh/helm/v3/pkg/downloader"
@ -171,6 +174,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")
addValueOptionsFlags(f, valueOpts)
addChartPathOptionsFlags(f, &client.ChartPathOptions)
addExternalFilesFlags(f, &client.ExternalFiles)
err := cmd.RegisterFlagCompletionFunc("version", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
requiredArgs := 2
@ -258,6 +262,11 @@ func runInstall(args []string, client *action.Install, valueOpts *values.Options
}
}
err = loadExternalFiles(chartRequested, client.ExternalFiles)
if err != nil {
return nil, err
}
client.Namespace = settings.Namespace()
// Create context and prepare the handle of SIGTERM
@ -289,6 +298,48 @@ func checkIfInstallable(ch *chart.Chart) error {
return errors.Errorf("%s charts are not installable", ch.Metadata.Type)
}
func loadExternalFiles(ch *chart.Chart, exFiles files.ExternalFiles) error {
var errs []string
fs := make(map[string]string)
for _, s := range exFiles.Files {
if err := files.ParseIntoString(s, fs); err != nil {
debug(fmt.Sprintf("error parsing include-file option %s: %v", s, err))
errs = append(errs, fmt.Sprintf("%s (parse error)", s))
}
}
for _, g := range exFiles.Globs {
if err := files.ParseIntoString(g, fs); err != nil {
debug(fmt.Sprintf("error parsing include-dir option %s: %v", g, err))
errs = append(errs, fmt.Sprintf("%s (parse error)", g))
}
}
for n, p := range fs {
allPaths, err := loader.ExpandLocalPath(n, p)
debug(fmt.Sprintf("%s expanded to: %v", p, allPaths))
if err != nil {
debug(fmt.Sprintf("error loading external path %s: %v", p, err))
errs = append(errs, fmt.Sprintf("%s (path not accessible)", p))
}
for name, fp := range allPaths {
byt, err := ioutil.ReadFile(fp)
if err != nil {
errs = append(errs, fmt.Sprintf("%s (not readable)", fp))
} else {
ch.Files = append(ch.Files, &chart.File{Name: name, Data: byt})
}
}
}
if len(errs) > 0 {
return errors.New(fmt.Sprint("Failed to load external files: ", strings.Join(errs, "; ")))
}
return nil
}
// Provide dynamic auto-completion for the install and template commands
func compInstall(args []string, toComplete string, client *action.Install) ([]string, cobra.ShellCompDirective) {
requiredArgs := 1

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

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

@ -0,0 +1 @@
glob-external-1

@ -0,0 +1 @@
glob-external-2

@ -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,22 @@
---
# 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: glob-external-1
external.2.conf: glob-external-2
external.txt: out-of-chart-dir

@ -0,0 +1,20 @@
---
# 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,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.1.conf: glob-external-1
external.2.conf: glob-external-2

@ -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: "glob/*"

@ -116,6 +116,7 @@ func newUpgradeCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
instClient.PostRenderer = client.PostRenderer
instClient.DisableOpenAPIValidation = client.DisableOpenAPIValidation
instClient.SubNotes = client.SubNotes
instClient.ExternalFiles = client.ExternalFiles
instClient.Description = client.Description
instClient.DependencyUpdate = client.DependencyUpdate
@ -181,6 +182,11 @@ func newUpgradeCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
warning("This chart is deprecated")
}
err = loadExternalFiles(ch, client.ExternalFiles)
if err != nil {
return err
}
// Create context and prepare the handle of SIGTERM
ctx := context.Background()
ctx, cancel := context.WithCancel(ctx)
@ -235,6 +241,7 @@ func newUpgradeCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
addValueOptionsFlags(f, valueOpts)
bindOutputFlag(cmd, &outfmt)
bindPostRenderFlag(cmd, &client.PostRenderer)
addExternalFilesFlags(f, &client.ExternalFiles)
err := cmd.RegisterFlagCompletionFunc("version", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
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: "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=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) {
tmpChart := ensure.TempDir(t)
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
}
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) {
outputFlagCompletionTest(t, "upgrade")
}

@ -41,6 +41,7 @@ import (
"helm.sh/helm/v3/pkg/chart"
"helm.sh/helm/v3/pkg/chartutil"
"helm.sh/helm/v3/pkg/cli"
"helm.sh/helm/v3/pkg/cli/files"
"helm.sh/helm/v3/pkg/downloader"
"helm.sh/helm/v3/pkg/getter"
"helm.sh/helm/v3/pkg/kube"
@ -100,6 +101,7 @@ type Install struct {
// OutputDir/<ReleaseName>
UseReleaseName bool
PostRenderer postrender.PostRenderer
ExternalFiles files.ExternalFiles
// Lock to control raceconditions when the process receives a SIGTERM
Lock sync.Mutex
}

@ -30,6 +30,7 @@ import (
"helm.sh/helm/v3/pkg/chart"
"helm.sh/helm/v3/pkg/chartutil"
"helm.sh/helm/v3/pkg/cli/files"
"helm.sh/helm/v3/pkg/kube"
"helm.sh/helm/v3/pkg/postrender"
"helm.sh/helm/v3/pkg/release"
@ -101,6 +102,7 @@ type Upgrade struct {
DisableOpenAPIValidation bool
// Get missing dependencies
DependencyUpdate bool
ExternalFiles files.ExternalFiles
// Lock to control raceconditions when the process receives a SIGTERM
Lock sync.Mutex
}

@ -0,0 +1,85 @@
/*
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 (
"errors"
"os"
"path/filepath"
"strings"
)
// ExpandLocalPath expands a local file, dir or glob path to a list of files
func ExpandLocalPath(name string, path string) (map[string]string, error) {
if strings.Contains(path, "*") {
// if this is a glob, we expand it and return a list of files
return expandGlob(name, path)
}
fi, err := os.Stat(path)
if err != nil {
return nil, err
}
if fi.IsDir() {
// if this is a valid dir, we return all files within
return expandDir(name, path)
}
// finally, this is a file, so we return it
return map[string]string{name: path}, nil
}
func expandGlob(name string, path string) (map[string]string, error) {
fmap := make(map[string]string)
paths, err := filepath.Glob(path)
if err != nil {
return nil, err
}
if len(paths) == 0 {
return nil, errors.New("empty glob")
}
namePrefix := strings.TrimRight(name, "/") + "/"
for _, p := range paths {
key := namePrefix + filepath.Base(p)
fmap[key] = p
}
return fmap, nil
}
func expandDir(name string, path string) (map[string]string, error) {
fmap := make(map[string]string)
f, err := os.Open(path)
if err != nil {
return nil, err
}
defer f.Close()
files, err := f.Readdir(-1)
if err != nil {
return nil, err
}
localDirName := strings.TrimRight(path, "/") + "/"
namePrefix := strings.TrimRight(name, "/") + "/"
for _, file := range files {
key := namePrefix + file.Name()
fmap[key] = localDirName + file.Name()
}
return fmap, nil
}

@ -0,0 +1,46 @@
/*
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/assert"
"github.com/stretchr/testify/require"
)
func TestExpandLocalPath(t *testing.T) {
need := require.New(t)
is := assert.New(t)
glob, err := ExpandLocalPath("glob", "testdata/frobnitz/*.yaml")
need.NoError(err)
need.Contains(glob, "glob/Chart.yaml")
need.Contains(glob, "glob/values.yaml")
is.Equal("testdata/frobnitz/Chart.yaml", glob["glob/Chart.yaml"])
is.Equal("testdata/frobnitz/values.yaml", glob["glob/values.yaml"])
dir, err := ExpandLocalPath("dir", "testdata/albatross/")
need.NoError(err)
need.Contains(dir, "dir/Chart.yaml")
need.Contains(dir, "dir/values.yaml")
is.Equal("testdata/albatross/Chart.yaml", dir["dir/Chart.yaml"])
is.Equal("testdata/albatross/values.yaml", dir["dir/values.yaml"])
file, err := ExpandLocalPath("file", "testdata/albatross/Chart.yaml")
need.NoError(err)
need.Contains(file, "file")
is.Equal("testdata/albatross/Chart.yaml", file["file"])
}

@ -0,0 +1,20 @@
/*
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 files
// ExternalFiles holds the list of external files or globs
type ExternalFiles struct {
Files []string
Globs []string
}

@ -0,0 +1,66 @@
/*
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 files
import (
"errors"
"fmt"
"path/filepath"
"strings"
)
// ParseIntoString parses a include-file line and merges the result into dest.
func ParseIntoString(s string, dest map[string]string) error {
for _, val := range strings.Split(s, ",") {
val = strings.TrimSpace(val)
splt := strings.SplitN(val, "=", 2)
if len(splt) != 2 {
return errors.New("Could not parse line")
}
name := strings.TrimSpace(splt[0])
path := strings.TrimSpace(splt[1])
dest[name] = path
}
return nil
}
//ParseGlobIntoString parses an include-dir file line and merges all files found into dest.
func ParseGlobIntoString(g string, dest map[string]string) error {
globs := make(map[string]string)
err := ParseIntoString(g, globs)
if err != nil {
return err
}
for k, g := range globs {
if !strings.Contains(g, "*") {
// force glob style on simple directories
g = strings.TrimRight(g, "/") + "/*"
}
paths, err := filepath.Glob(g)
if err != nil {
return err
}
k = strings.TrimRight(k, "/")
for _, path := range paths {
dest[fmt.Sprintf("%s/%s", k, filepath.Base(path))] = path
}
}
return nil
}

@ -0,0 +1,76 @@
/*
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 files
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestParseIntoString(t *testing.T) {
need := require.New(t)
is := assert.New(t)
dest := make(map[string]string)
goodFlag := "foo.txt=../foo.txt"
anotherFlag := " bar.txt=~/bar.txt, baz.txt=/path/to/baz.txt"
err := ParseIntoString(goodFlag, dest)
need.NoError(err)
err = ParseIntoString(anotherFlag, dest)
need.NoError(err)
is.Contains(dest, "foo.txt")
is.Contains(dest, "bar.txt")
is.Contains(dest, "baz.txt")
is.Equal(dest["foo.txt"], "../foo.txt", "foo.txt not mapped properly")
is.Equal(dest["bar.txt"], "~/bar.txt", "bar.txt not mapped properly")
is.Equal(dest["baz.txt"], "/path/to/baz.txt", "baz.txt not mapped properly")
overwriteFlag := "foo.txt=../new_foo.txt"
err = ParseIntoString(overwriteFlag, dest)
need.NoError(err)
is.Equal(dest["foo.txt"], "../new_foo.txt")
badFlag := "empty.txt"
err = ParseIntoString(badFlag, dest)
is.NotNil(err)
}
func TestParseGlobIntoString(t *testing.T) {
need := require.New(t)
is := assert.New(t)
dest := make(map[string]string)
globFlagSlash := "glob/=testdata/foo/foo.*"
dirFlagNoSlash := "dir=testdata/foo/"
err := ParseGlobIntoString(globFlagSlash, dest)
need.NoError(err)
need.Contains(dest, "glob/foo.txt")
is.Equal("testdata/foo/foo.txt", dest["glob/foo.txt"])
err = ParseGlobIntoString(dirFlagNoSlash, dest)
need.NoError(err)
need.Contains(dest, "dir/foo.txt")
need.Contains(dest, "dir/bar.txt")
is.Equal("testdata/foo/foo.txt", dest["dir/foo.txt"])
is.Equal("testdata/foo/bar.txt", dest["dir/bar.txt"])
}
Loading…
Cancel
Save