diff options
| author | Dave Henderson <dhenderson@gmail.com> | 2018-08-09 16:52:21 -0400 |
|---|---|---|
| committer | Dave Henderson <dhenderson@gmail.com> | 2018-08-10 22:11:46 -0400 |
| commit | 22ae7244297c3b63cfb8f44e3a9be52286e629df (patch) | |
| tree | f3b643830254378c0c75e9bf2d0760f93b672720 | |
| parent | fbb3f3f9dde228494b8d91698d7e11b749d2b88e (diff) | |
Adding --chmod flag to explicitly set output file modes
Signed-off-by: Dave Henderson <dhenderson@gmail.com>
| -rw-r--r-- | cmd/gomplate/main.go | 13 | ||||
| -rw-r--r-- | docs/content/usage.md | 6 | ||||
| -rw-r--r-- | gomplate.go | 13 | ||||
| -rw-r--r-- | template.go | 68 | ||||
| -rw-r--r-- | template_test.go | 120 | ||||
| -rw-r--r-- | tests/integration/basic_test.go | 92 | ||||
| -rw-r--r-- | tests/integration/inputdir_test.go | 53 |
7 files changed, 318 insertions, 47 deletions
diff --git a/cmd/gomplate/main.go b/cmd/gomplate/main.go index bde6eb46..c0ba8909 100644 --- a/cmd/gomplate/main.go +++ b/cmd/gomplate/main.go @@ -93,22 +93,27 @@ func newGomplateCmd() *cobra.Command { } func initFlags(command *cobra.Command) { - command.Flags().BoolVarP(&printVer, "version", "v", false, "print the version") + command.Flags().SortFlags = false + + command.Flags().StringArrayVarP(&opts.DataSources, "datasource", "d", nil, "`datasource` in alias=URL form. Specify multiple times to add multiple sources.") + command.Flags().StringArrayVarP(&opts.DataSourceHeaders, "datasource-header", "H", nil, "HTTP `header` field in 'alias=Name: value' form to be provided on HTTP-based data sources. Multiples can be set.") command.Flags().StringArrayVarP(&opts.InputFiles, "file", "f", []string{"-"}, "Template `file` to process. Omit to use standard input, or use --in or --input-dir") command.Flags().StringVarP(&opts.Input, "in", "i", "", "Template `string` to process (alternative to --file and --input-dir)") command.Flags().StringVar(&opts.InputDir, "input-dir", "", "`directory` which is examined recursively for templates (alternative to --file and --in)") + command.Flags().StringArrayVar(&opts.ExcludeGlob, "exclude", []string{}, "glob of files to not parse") + command.Flags().StringArrayVarP(&opts.OutputFiles, "out", "o", []string{"-"}, "output `file` name. Omit to use standard output.") command.Flags().StringVar(&opts.OutputDir, "output-dir", ".", "`directory` to store the processed templates. Only used for --input-dir") - - command.Flags().StringArrayVarP(&opts.DataSources, "datasource", "d", nil, "`datasource` in alias=URL form. Specify multiple times to add multiple sources.") - command.Flags().StringArrayVarP(&opts.DataSourceHeaders, "datasource-header", "H", nil, "HTTP `header` field in 'alias=Name: value' form to be provided on HTTP-based data sources. Multiples can be set.") + command.Flags().StringVar(&opts.OutMode, "chmod", "", "set the mode for output file(s). Omit to inherit from input file(s)") ldDefault := env.Getenv("GOMPLATE_LEFT_DELIM", "{{") rdDefault := env.Getenv("GOMPLATE_RIGHT_DELIM", "}}") command.Flags().StringVar(&opts.LDelim, "left-delim", ldDefault, "override the default left-`delimiter` [$GOMPLATE_LEFT_DELIM]") command.Flags().StringVar(&opts.RDelim, "right-delim", rdDefault, "override the default right-`delimiter` [$GOMPLATE_RIGHT_DELIM]") + + command.Flags().BoolVarP(&printVer, "version", "v", false, "print the version") } func main() { diff --git a/docs/content/usage.md b/docs/content/usage.md index ee203c08..4631a923 100644 --- a/docs/content/usage.md +++ b/docs/content/usage.md @@ -43,6 +43,12 @@ Example: gomplate --input-dir=templates --output-dir=config --datasource config=config.yaml ``` +### `--chmod` + +By default, output files are created with the same file mode (permissions) as input files. If desired, the `--chmod` option can be used to override this behaviour, and set the output file mode explicitly. This can be useful for creating executable scripts or ensuring write permissions. + +The value must be an octal integer in the standard UNIX `chmod` format, i.e. `644` to indicate that owner gets read+write, group gets read-only, and others get read-only permissions. See the [`chmod(1)` man page](https://linux.die.net/man/1/chmod) for more details. + ### `--exclude` To prevent certain files from being processed, you can use `--exclude`. It takes a glob, and any files matching that glob will not be included. diff --git a/gomplate.go b/gomplate.go index f5d17af9..11a8ae8f 100644 --- a/gomplate.go +++ b/gomplate.go @@ -3,6 +3,7 @@ package gomplate import ( "io" "os" + "strconv" "text/template" "time" @@ -18,6 +19,7 @@ type Config struct { ExcludeGlob []string OutputFiles []string OutputDir string + OutMode string DataSources []string DataSourceHeaders []string @@ -26,6 +28,17 @@ type Config struct { RDelim string } +// parse an os.FileMode out of the string, and let us know if it's an override or not... +func (o *Config) getMode() (os.FileMode, bool, error) { + modeOverride := o.OutMode != "" + m, err := strconv.ParseUint("0"+o.OutMode, 8, 32) + if err != nil { + return 0, false, err + } + mode := os.FileMode(m) + return mode, modeOverride, nil +} + // gomplate - type gomplate struct { funcMap template.FuncMap diff --git a/template.go b/template.go index dcb2c8b1..496ee907 100644 --- a/template.go +++ b/template.go @@ -20,11 +20,12 @@ var Stdout io.WriteCloser = os.Stdout // tplate - models a gomplate template file... type tplate struct { - name string - targetPath string - target io.Writer - contents string - mode os.FileMode + name string + targetPath string + target io.Writer + contents string + mode os.FileMode + modeOverride bool } func (t *tplate) toGoTemplate(g *gomplate) (*template.Template, error) { @@ -48,19 +49,25 @@ func (t *tplate) addTarget() (err error) { t.targetPath = "-" } if t.target == nil { - t.target, err = openOutFile(t.targetPath, t.mode) + t.target, err = openOutFile(t.targetPath, t.mode, t.modeOverride) } return err } // gatherTemplates - gather and prepare input template(s) and output file(s) for rendering func gatherTemplates(o *Config) (templates []*tplate, err error) { + mode, modeOverride, err := o.getMode() + // the arg-provided input string gets a special name if o.Input != "" { + if mode == 0 { + mode = 0644 + } templates = []*tplate{{ - name: "<arg>", - contents: o.Input, - mode: os.FileMode(0644), + name: "<arg>", + contents: o.Input, + mode: mode, + modeOverride: modeOverride, }} if len(o.OutputFiles) == 1 { @@ -70,14 +77,14 @@ func gatherTemplates(o *Config) (templates []*tplate, err error) { // input dirs presume output dirs are set too if o.InputDir != "" { - templates, err = walkDir(o.InputDir, o.OutputDir, o.ExcludeGlob) + templates, err = walkDir(o.InputDir, o.OutputDir, o.ExcludeGlob, mode, modeOverride) if err != nil { return nil, err } } else if len(o.InputFiles) > 0 && o.Input == "" { templates = make([]*tplate, len(o.InputFiles)) for i := range o.InputFiles { - templates[i], err = fileToTemplates(o.InputFiles[i], o.OutputFiles[i]) + templates[i], err = fileToTemplates(o.InputFiles[i], o.OutputFiles[i], mode, modeOverride) if err != nil { return nil, err } @@ -104,7 +111,7 @@ func processTemplates(templates []*tplate) ([]*tplate, error) { // walkDir - given an input dir `dir` and an output dir `outDir`, and a list // of exclude globs (if any), walk the input directory and create a list of // tplate objects, and an error, if any. -func walkDir(dir, outDir string, excludeGlob []string) ([]*tplate, error) { +func walkDir(dir, outDir string, excludeGlob []string, mode os.FileMode, modeOverride bool) ([]*tplate, error) { dir = filepath.Clean(dir) outDir = filepath.Clean(outDir) si, err := fs.Stat(dir) @@ -136,35 +143,41 @@ func walkDir(dir, outDir string, excludeGlob []string) ([]*tplate, error) { } if entry.IsDir() { - t, err := walkDir(nextInPath, nextOutPath, excludes) + t, err := walkDir(nextInPath, nextOutPath, excludes, mode, modeOverride) if err != nil { return nil, err } templates = append(templates, t...) } else { + if mode == 0 { + mode = entry.Mode() + } templates = append(templates, &tplate{ - name: nextInPath, - targetPath: nextOutPath, - mode: entry.Mode(), + name: nextInPath, + targetPath: nextOutPath, + mode: mode, + modeOverride: modeOverride, }) } } return templates, nil } -func fileToTemplates(inFile, outFile string) (*tplate, error) { - mode := os.FileMode(0644) +func fileToTemplates(inFile, outFile string, mode os.FileMode, modeOverride bool) (*tplate, error) { if inFile != "-" { si, err := fs.Stat(inFile) if err != nil { return nil, err } - mode = si.Mode() + if mode == 0 { + mode = si.Mode() + } } tmpl := &tplate{ - name: inFile, - targetPath: outFile, - mode: mode, + name: inFile, + targetPath: outFile, + mode: mode, + modeOverride: modeOverride, } return tmpl, nil @@ -180,11 +193,18 @@ func inList(list []string, entry string) bool { return false } -func openOutFile(filename string, mode os.FileMode) (out io.WriteCloser, err error) { +func openOutFile(filename string, mode os.FileMode, modeOverride bool) (out io.WriteCloser, err error) { if filename == "-" { return Stdout, nil } - return fs.OpenFile(filename, os.O_RDWR|os.O_CREATE|os.O_TRUNC, mode.Perm()) + out, err = fs.OpenFile(filename, os.O_RDWR|os.O_CREATE|os.O_TRUNC, mode.Perm()) + if err != nil { + return out, err + } + if modeOverride { + err = fs.Chmod(filename, mode.Perm()) + } + return out, err } func readInput(filename string) (string, error) { diff --git a/template_test.go b/template_test.go index 721ba4a5..190fb35f 100644 --- a/template_test.go +++ b/template_test.go @@ -4,6 +4,7 @@ package gomplate import ( "bytes" + "io" "io/ioutil" "os" "testing" @@ -45,7 +46,7 @@ func TestOpenOutFile(t *testing.T) { fs = afero.NewMemMapFs() _ = fs.Mkdir("/tmp", 0777) - _, err := openOutFile("/tmp/foo", os.FileMode(0644)) + _, err := openOutFile("/tmp/foo", 0644, false) assert.NoError(t, err) i, err := fs.Stat("/tmp/foo") assert.NoError(t, err) @@ -54,7 +55,7 @@ func TestOpenOutFile(t *testing.T) { defer func() { Stdout = os.Stdout }() Stdout = &nopWCloser{&bytes.Buffer{}} - f, err := openOutFile("-", os.FileMode(0644)) + f, err := openOutFile("-", 0644, false) assert.NoError(t, err) assert.Equal(t, Stdout, f) } @@ -94,7 +95,7 @@ func TestWalkDir(t *testing.T) { defer func() { fs = origfs }() fs = afero.NewMemMapFs() - _, err := walkDir("/indir", "/outdir", nil) + _, err := walkDir("/indir", "/outdir", nil, 0, false) assert.Error(t, err) _ = fs.MkdirAll("/indir/one", 0777) @@ -103,7 +104,7 @@ func TestWalkDir(t *testing.T) { afero.WriteFile(fs, "/indir/one/bar", []byte("bar"), 0644) afero.WriteFile(fs, "/indir/two/baz", []byte("baz"), 0644) - templates, err := walkDir("/indir", "/outdir", []string{"/*/two"}) + templates, err := walkDir("/indir", "/outdir", []string{"/*/two"}, 0, false) assert.NoError(t, err) assert.Equal(t, 2, len(templates)) @@ -141,7 +142,7 @@ func TestGatherTemplates(t *testing.T) { origfs := fs defer func() { fs = origfs }() fs = afero.NewMemMapFs() - afero.WriteFile(fs, "foo", []byte("bar"), 0644) + afero.WriteFile(fs, "foo", []byte("bar"), 0600) afero.WriteFile(fs, "in/1", []byte("foo"), 0644) afero.WriteFile(fs, "in/2", []byte("bar"), 0644) @@ -167,6 +168,10 @@ func TestGatherTemplates(t *testing.T) { assert.Len(t, templates, 1) assert.Equal(t, "out", templates[0].targetPath) assert.Equal(t, os.FileMode(0644), templates[0].mode) + info, err := fs.Stat("out") + assert.NoError(t, err) + assert.Equal(t, os.FileMode(0644), info.Mode()) + fs.Remove("out") templates, err = gatherTemplates(&Config{ InputFiles: []string{"foo"}, @@ -176,6 +181,26 @@ func TestGatherTemplates(t *testing.T) { assert.Len(t, templates, 1) assert.Equal(t, "bar", templates[0].contents) assert.NotEqual(t, Stdout, templates[0].target) + assert.Equal(t, os.FileMode(0600), templates[0].mode) + info, err = fs.Stat("out") + assert.NoError(t, err) + assert.Equal(t, os.FileMode(0600), info.Mode()) + fs.Remove("out") + + templates, err = gatherTemplates(&Config{ + InputFiles: []string{"foo"}, + OutputFiles: []string{"out"}, + OutMode: "755", + }) + assert.NoError(t, err) + assert.Len(t, templates, 1) + assert.Equal(t, "bar", templates[0].contents) + assert.NotEqual(t, Stdout, templates[0].target) + assert.Equal(t, os.FileMode(0755), templates[0].mode) + info, err = fs.Stat("out") + assert.NoError(t, err) + assert.Equal(t, os.FileMode(0755), info.Mode()) + fs.Remove("out") templates, err = gatherTemplates(&Config{ InputDir: "in", @@ -184,4 +209,89 @@ func TestGatherTemplates(t *testing.T) { assert.NoError(t, err) assert.Len(t, templates, 3) assert.Equal(t, "foo", templates[0].contents) + fs.Remove("out") +} + +func TestProcessTemplates(t *testing.T) { + origfs := fs + defer func() { fs = origfs }() + fs = afero.NewMemMapFs() + afero.WriteFile(fs, "foo", []byte("bar"), 0600) + + afero.WriteFile(fs, "in/1", []byte("foo"), 0644) + afero.WriteFile(fs, "in/2", []byte("bar"), 0640) + afero.WriteFile(fs, "in/3", []byte("baz"), 0644) + + afero.WriteFile(fs, "existing", []byte(""), 0644) + + testdata := []struct { + templates []*tplate + contents []string + modes []os.FileMode + targets []io.WriteCloser + }{ + {}, + { + templates: []*tplate{{name: "<arg>", contents: "foo", targetPath: "-", mode: 0644}}, + contents: []string{"foo"}, + modes: []os.FileMode{0644}, + targets: []io.WriteCloser{Stdout}, + }, + { + templates: []*tplate{{name: "<arg>", contents: "foo", targetPath: "out", mode: 0644}}, + contents: []string{"foo"}, + modes: []os.FileMode{0644}, + }, + { + templates: []*tplate{{name: "foo", targetPath: "out", mode: 0600}}, + contents: []string{"bar"}, + modes: []os.FileMode{0600}, + }, + { + templates: []*tplate{{name: "foo", targetPath: "out", mode: 0755}}, + contents: []string{"bar"}, + modes: []os.FileMode{0755}, + }, + { + templates: []*tplate{ + {name: "in/1", targetPath: "out/1", mode: 0644}, + {name: "in/2", targetPath: "out/2", mode: 0640}, + {name: "in/3", targetPath: "out/3", mode: 0644}, + }, + contents: []string{"foo", "bar", "baz"}, + modes: []os.FileMode{0644, 0640, 0644}, + }, + { + templates: []*tplate{ + {name: "foo", targetPath: "existing", mode: 0755}, + }, + contents: []string{"bar"}, + modes: []os.FileMode{0644}, + }, + { + templates: []*tplate{ + {name: "foo", targetPath: "existing", mode: 0755, modeOverride: true}, + }, + contents: []string{"bar"}, + modes: []os.FileMode{0755}, + }, + } + for _, in := range testdata { + actual, err := processTemplates(in.templates) + assert.NoError(t, err) + assert.Len(t, actual, len(in.templates)) + for i, a := range actual { + assert.Equal(t, in.contents[i], a.contents) + assert.Equal(t, in.templates[i].mode, a.mode) + if len(in.targets) > 0 { + assert.Equal(t, in.targets[i], a.target) + } + if in.templates[i].targetPath != "-" { + info, err := fs.Stat(in.templates[i].targetPath) + assert.NoError(t, err) + assert.Equal(t, os.FileMode(in.modes[i]), info.Mode()) + } + } + fs.Remove("out") + } } diff --git a/tests/integration/basic_test.go b/tests/integration/basic_test.go index de890905..349efe04 100644 --- a/tests/integration/basic_test.go +++ b/tests/integration/basic_test.go @@ -1,11 +1,13 @@ -// +build integration +//+build integration //+build !windows package integration import ( "bytes" + "fmt" "io/ioutil" + "os" . "gopkg.in/check.v1" @@ -21,13 +23,13 @@ type BasicSuite struct { var _ = Suite(&BasicSuite{}) -func (s *BasicSuite) SetUpSuite(c *C) { +func (s *BasicSuite) SetUpTest(c *C) { s.tmpDir = fs.NewDir(c, "gomplate-inttests", - fs.WithFile("one", "hi\n"), + fs.WithFile("one", "hi\n", fs.WithMode(0640)), fs.WithFile("two", "hello\n")) } -func (s *BasicSuite) TearDownSuite(c *C) { +func (s *BasicSuite) TearDownTest(c *C) { s.tmpDir.Remove() } @@ -89,12 +91,82 @@ func (s *BasicSuite) TestRoutesInputsToProperOutputs(c *C) { }) result.Assert(c, icmd.Success) - content, err := ioutil.ReadFile(oneOut) - assert.NilError(c, err) - assert.Equal(c, "hi\n", string(content)) - content, err = ioutil.ReadFile(twoOut) - assert.NilError(c, err) - assert.Equal(c, "hello\n", string(content)) + testdata := []struct { + path string + mode os.FileMode + content string + }{ + {oneOut, 0640, "hi\n"}, + {twoOut, 0644, "hello\n"}, + } + for _, v := range testdata { + info, err := os.Stat(v.path) + assert.NilError(c, err) + assert.Equal(c, v.mode, info.Mode()) + content, err := ioutil.ReadFile(v.path) + assert.NilError(c, err) + assert.Equal(c, v.content, string(content)) + } +} + +func (s *BasicSuite) TestRoutesInputsToProperOutputsWithChmod(c *C) { + oneOut := s.tmpDir.Join("one.out") + twoOut := s.tmpDir.Join("two.out") + result := icmd.RunCmd(icmd.Command(GomplateBin, + "-f", s.tmpDir.Join("one"), + "-f", s.tmpDir.Join("two"), + "-o", oneOut, + "-o", twoOut, + "--chmod", "0600"), func(cmd *icmd.Cmd) { + cmd.Stdin = bytes.NewBufferString("hello world") + }) + result.Assert(c, icmd.Success) + fmt.Println(result.Combined()) + + testdata := []struct { + path string + mode os.FileMode + content string + }{ + {oneOut, 0600, "hi\n"}, + {twoOut, 0600, "hello\n"}, + } + for _, v := range testdata { + info, err := os.Stat(v.path) + assert.NilError(c, err) + assert.Equal(c, v.mode, info.Mode()) + content, err := ioutil.ReadFile(v.path) + assert.NilError(c, err) + assert.Equal(c, v.content, string(content)) + } +} + +func (s *BasicSuite) TestOverridesOutputModeWithChmod(c *C) { + out := s.tmpDir.Join("two") + result := icmd.RunCmd(icmd.Command(GomplateBin, + "-f", s.tmpDir.Join("one"), + "-o", out, + "--chmod", "0600"), func(cmd *icmd.Cmd) { + cmd.Stdin = bytes.NewBufferString("hello world") + }) + result.Assert(c, icmd.Success) + fmt.Println(result.Combined()) + + testdata := []struct { + path string + mode os.FileMode + content string + }{ + {out, 0600, "hi\n"}, + } + for _, v := range testdata { + info, err := os.Stat(v.path) + assert.NilError(c, err) + assert.Equal(c, v.mode, info.Mode()) + content, err := ioutil.ReadFile(v.path) + assert.NilError(c, err) + assert.Equal(c, v.content, string(content)) + } } func (s *BasicSuite) TestFlagRules(c *C) { diff --git a/tests/integration/inputdir_test.go b/tests/integration/inputdir_test.go index 2230e5a1..d1125c53 100644 --- a/tests/integration/inputdir_test.go +++ b/tests/integration/inputdir_test.go @@ -5,6 +5,7 @@ package integration import ( "io/ioutil" + "os" . "gopkg.in/check.v1" @@ -56,13 +57,57 @@ func (s *InputDirSuite) TestInputDir(c *C) { assert.NilError(c, err) tassert.Len(c, files, 1) - content, err := ioutil.ReadFile(s.tmpDir.Join("out", "eins.txt")) + testdata := []struct { + path string + mode os.FileMode + content string + }{ + {s.tmpDir.Join("out", "eins.txt"), 0644, "eins"}, + {s.tmpDir.Join("out", "inner", "deux.txt"), 0644, "deux"}, + } + for _, v := range testdata { + info, err := os.Stat(v.path) + assert.NilError(c, err) + assert.Equal(c, v.mode, info.Mode()) + content, err := ioutil.ReadFile(v.path) + assert.NilError(c, err) + assert.Equal(c, v.content, string(content)) + } +} + +func (s *InputDirSuite) TestInputDirWithModeOverride(c *C) { + result := icmd.RunCommand(GomplateBin, + "--input-dir", s.tmpDir.Join("in"), + "--output-dir", s.tmpDir.Join("out"), + "--chmod", "0601", + "-d", "config="+s.tmpDir.Join("config.yml"), + ) + result.Assert(c, icmd.Success) + + files, err := ioutil.ReadDir(s.tmpDir.Join("out")) assert.NilError(c, err) - assert.Equal(c, "eins", string(content)) + tassert.Len(c, files, 2) - content, err = ioutil.ReadFile(s.tmpDir.Join("out", "inner", "deux.txt")) + files, err = ioutil.ReadDir(s.tmpDir.Join("out", "inner")) assert.NilError(c, err) - assert.Equal(c, "deux", string(content)) + tassert.Len(c, files, 1) + + testdata := []struct { + path string + mode os.FileMode + content string + }{ + {s.tmpDir.Join("out", "eins.txt"), 0601, "eins"}, + {s.tmpDir.Join("out", "inner", "deux.txt"), 0601, "deux"}, + } + for _, v := range testdata { + info, err := os.Stat(v.path) + assert.NilError(c, err) + assert.Equal(c, v.mode, info.Mode()) + content, err := ioutil.ReadFile(v.path) + assert.NilError(c, err) + assert.Equal(c, v.content, string(content)) + } } func (s *InputDirSuite) TestDefaultOutputDir(c *C) { |
