diff --git a/README.md b/README.md index 313e2492..40b15319 100644 --- a/README.md +++ b/README.md @@ -155,6 +155,7 @@ Learn more about boilerplate in the following sections: 1. [Dependencies](#dependencies) 1. [Hooks](#hooks) 1. [Skip Files](#skip-files) +1. [Skip Templating](#skip-files) 1. [Templates](#templates) 1. [Validations](#validations) 1. [Variable Ordering](#variable-ordering) @@ -312,6 +313,9 @@ skip_files: - path: subfolder/README.md if: {{ not .ShowLogo }} +skip_templating: + - path: subfolder/helm-charts/* + engines: - path: subfolder/foo.json.jsonnet template_engine: jsonnet @@ -392,6 +396,16 @@ values `"true"` or `"false"`. See the [Skip Files](#skip-files) section for more info. +**Skip Templating**: Use *skip_templating* to specify files in the template folder that should not be parsed by the templating engine but will be copied to the `output-folder` with the same path and file name . The `path` field +is the relative path from the template folder root (where the `boilerplate.yml` file is defined) of the file that should +be copied without rendering. You can conditionally skip the templating of the file using the `if` field, which should either be a YAML boolean value +(`true` or `false`), or Go templating syntax (which can use the boilerplate variables) that evaluates to the string +values `"true"` or `"false"`. + +See the [Skip Files](#skip-files) section for more info on the configuration as the syntax is the same. + +If a file is listed in both `skip_files` and `skip_templating`, the file will be skipped entirely. + **Engines**: Use *engines* to specify files in the template folder that should be rendered with an alternative templating engine. The `path` field is the relative path from the template folder root (where the `boilerplate.yml` file is defined) of the file that should use the alternative template engine. @@ -586,11 +600,11 @@ Note the following: #### Skip Files -You can specify files that should be excluded from the rendered output using the `skip_files` section in -`boilerplate.yml`. This is most useful when you have templates that need to conditionally exclude files from the -rendered folder list. +You can specify files that should be excluded from the rendered output using the `skip_files`, or copied without rendering using the `skip_templating` sections in +`boilerplate.yml`. `skip_files` is most useful when you have templates that need to conditionally exclude files from the +rendered folder list. `skip_templating` is useful when you want to copy files without rendering them, such as files that contain syntax similar to the go templating syntax - such as helm charts. -The `skip_files` section is a list of objects with the fields `path`, `not_path`, and `if`, where one of `path` or +The `skip_files` and `skip_templating` sections are a list of objects with the fields `path`, `not_path`, and `if`, where one of `path` or `not_path` is required. When `path` is set, all files that match the `path` attribute will be skipped, while when `not_path` is set, all files that DO NOT match the `not_path` attribute are skipped (in other words, only paths that match `not_path` are kept). @@ -608,6 +622,9 @@ Consider the following boilerplate template folder: . ├── boilerplate.yml ├── BOILERPLATE_README.md +├── helm-charts + ├── Chart.yaml + └── values.yaml └── docs ├── README_WITH_ENCRYPTION.md └── README_WITHOUT_ENCRYPTION.md @@ -638,11 +655,25 @@ This will: - Skip rendering `docs/README_WITH_ENCRYPTION.md` if `UseEncryption` is set to `false`. - If `DocsOnly` is set to `true`, only render the `docs` folder. +Also suppose that you wanted to conditionally select which helm chart file to simly be copied, while rendering the variables. You can use `skip_templating` +to implement this: + +```yaml +skip_templating: + - path: "*/Chart.yaml" +``` + +This will: + +- Copy `helm-charts/Chart.yaml` to the `output-folder` without processing the file. + For a more concise specification, you can use glob syntax in the `path` to match multiple paths in one entry: ```yaml skip_files: - path: "docs/**/*" +skip_templating: + - path: "helm-charts/*" ``` diff --git a/config/config.go b/config/config.go index b2d8b228..d174d37e 100644 --- a/config/config.go +++ b/config/config.go @@ -27,6 +27,7 @@ type BoilerplateConfig struct { Hooks variables.Hooks Partials []string SkipFiles []variables.SkipFile + SkipTemplating []variables.SkipFile Engines []variables.Engine } @@ -76,11 +77,17 @@ func (config *BoilerplateConfig) UnmarshalYAML(unmarshal func(interface{}) error return err } - skipFiles, err := variables.UnmarshalSkipFilesFromBoilerplateConfigYaml(fields) + skipFiles, err := variables.UnmarshalSkipFilesFromBoilerplateConfigYaml(fields, "skip_files") if err != nil { return err } + skipTemplating, err := variables.UnmarshalSkipFilesFromBoilerplateConfigYaml(fields, "skip_templating") + if err != nil { + return err + } + + engines, err := variables.UnmarshalEnginesFromBoilerplateConfigYaml(fields) if err != nil { return err @@ -93,6 +100,7 @@ func (config *BoilerplateConfig) UnmarshalYAML(unmarshal func(interface{}) error Hooks: hooks, Partials: partials, SkipFiles: skipFiles, + SkipTemplating: skipTemplating, Engines: engines, } return nil @@ -154,6 +162,20 @@ func (config *BoilerplateConfig) MarshalYAML() (interface{}, error) { } configYml["skip_files"] = skipFilesYml } + if len(config.SkipTemplating) > 0 { + // Due to go type system, we can only pass through []interface{}, even though []SkipTemplating is technically + // polymorphic to that type. So we reconstruct the list using the right type before passing it in to the marshal + // function. + interfaceList := []interface{}{} + for _, skipTemplate := range config.SkipTemplating { + interfaceList = append(interfaceList, skipTemplate) + } + skipTemplatingYml, err := util.MarshalListOfObjectsToYAML(interfaceList) + if err != nil { + return nil, err + } + configYml["skip_templating"] = skipTemplatingYml + } if len(config.Engines) > 0 { // Due to go type system, we can only pass through []interface{}, even though []Engine is technically // polymorphic to that type. So we reconstruct the list using the right type before passing it in to the marshal diff --git a/config/config_test.go b/config/config_test.go index 57d9e45e..9eb45888 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -711,6 +711,34 @@ func TestParseBoilerplateConfigWithSkipFiles(t *testing.T) { assert.Equal(t, expected, actual) } +// YAML is whitespace sensitive, so we need to be careful that we don't introduce unnecessary indentation +const configWithSkipTemplating = `skip_templating: + - path: "docs/README_ALWAYS_SKIP.md" + - path: "docs/README_MAYBE_SKIP.md" + if: "{{ .MaybeSkip }}" + - not_path: "docs/**/*" + if: "{{ .DocsOnly }}" +` + +func TestParseBoilerplateConfigWithSkipTemplating(t *testing.T) { + t.Parallel() + + actual, err := ParseBoilerplateConfig([]byte(configWithSkipTemplating)) + require.NoError(t, err) + + expected := &BoilerplateConfig{ + Variables: []variables.Variable{}, + Dependencies: []variables.Dependency{}, + Hooks: variables.Hooks{}, + SkipTemplating: []variables.SkipFile{ + {Path: "docs/README_ALWAYS_SKIP.md", If: ""}, + {Path: "docs/README_MAYBE_SKIP.md", If: "{{ .MaybeSkip }}"}, + {NotPath: "docs/**/*", If: "{{ .DocsOnly }}"}, + }, + } + assert.Equal(t, expected, actual) +} + func TestMarshalBoilerplateConfig(t *testing.T) { t.Parallel() diff --git a/examples/for-learning-and-testing/skip-templating/a/b/c/d.yml b/examples/for-learning-and-testing/skip-templating/a/b/c/d.yml new file mode 100644 index 00000000..464d68f8 --- /dev/null +++ b/examples/for-learning-and-testing/skip-templating/a/b/c/d.yml @@ -0,0 +1 @@ +val: {{ VarVal }} diff --git a/examples/for-learning-and-testing/skip-templating/a/b/c/e.yml b/examples/for-learning-and-testing/skip-templating/a/b/c/e.yml new file mode 100644 index 00000000..464d68f8 --- /dev/null +++ b/examples/for-learning-and-testing/skip-templating/a/b/c/e.yml @@ -0,0 +1 @@ +val: {{ VarVal }} diff --git a/examples/for-learning-and-testing/skip-templating/boilerplate.yml b/examples/for-learning-and-testing/skip-templating/boilerplate.yml new file mode 100644 index 00000000..1cec0d9d --- /dev/null +++ b/examples/for-learning-and-testing/skip-templating/boilerplate.yml @@ -0,0 +1,7 @@ +variables: + - name: VarVal + type: string + default: "val" + +skip_templating: + - path: "**/d.yml" diff --git a/templates/template_processor.go b/templates/template_processor.go index e45dccb3..415608e3 100644 --- a/templates/template_processor.go +++ b/templates/template_processor.go @@ -458,11 +458,15 @@ func processTemplateFolder( ) error { util.Logger.Printf("Processing templates in %s and outputting generated files to %s", opts.TemplateFolder, opts.OutputFolder) - // Process and render skip files and engines before walking so we only do the rendering operation once. + // Process and render skip files, templates and engines before walking so we only do the rendering operation once. processedSkipFiles, err := processSkipFiles(config.SkipFiles, opts, variables) if err != nil { return err } + processedSkipTemplating, err := processSkipFiles(config.SkipTemplating, opts, variables) + if err != nil { + return err + } processedEngines, err := processEngines(config.Engines, opts, variables) if err != nil { return err @@ -477,7 +481,8 @@ func processTemplateFolder( return createOutputDir(path, opts, variables) } else { engine := determineTemplateEngine(processedEngines, path) - return processFile(path, opts, variables, partials, engine) + skipTemplating := shouldSkipPath(path, opts, processedSkipTemplating) + return processFile(path, opts, variables, partials, skipTemplating, engine) } }) } @@ -489,8 +494,13 @@ func processFile( opts *options.BoilerplateOptions, variables map[string]interface{}, partials []string, + skipTemplating bool, engine variables.TemplateEngineType, ) error { + if skipTemplating { + util.Logger.Printf("Skipping templating of %s", path) + return copyFile(path, opts, variables) + } isText, err := util.IsTextFile(path) if err != nil { return err diff --git a/test-fixtures/examples-expected-output/skip-templating/a/b/c/d.yml b/test-fixtures/examples-expected-output/skip-templating/a/b/c/d.yml new file mode 100644 index 00000000..464d68f8 --- /dev/null +++ b/test-fixtures/examples-expected-output/skip-templating/a/b/c/d.yml @@ -0,0 +1 @@ +val: {{ VarVal }} diff --git a/test-fixtures/examples-expected-output/skip-templating/a/b/c/e.yml b/test-fixtures/examples-expected-output/skip-templating/a/b/c/e.yml new file mode 100644 index 00000000..d36492cf --- /dev/null +++ b/test-fixtures/examples-expected-output/skip-templating/a/b/c/e.yml @@ -0,0 +1 @@ +val: val diff --git a/test-fixtures/examples-var-files/skip-templating/vars.yml b/test-fixtures/examples-var-files/skip-templating/vars.yml new file mode 100644 index 00000000..e69de29b diff --git a/variables/skip_files.go b/variables/skip_files.go index 4f4f9ab5..5d4c9e7c 100644 --- a/variables/skip_files.go +++ b/variables/skip_files.go @@ -2,7 +2,7 @@ package variables import "github.com/gruntwork-io/boilerplate/errors" -// A single skip_file entry, which is a file that (conditionally) should be excluded from the rendered output. +// A single skip_file or skip_templating entry, which is a file that (conditionally) should be excluded from the rendered output. type SkipFile struct { Path string NotPath string @@ -27,15 +27,15 @@ func (skipFile SkipFile) MarshalYAML() (interface{}, error) { // Given a list of key:value pairs read from a Boilerplate YAML config file of the format: // -// skip_files: +// (skip_files|skip_templating): // - path: // if: // - path: // - not_path: // // convert to a list of SkipFile structs. -func UnmarshalSkipFilesFromBoilerplateConfigYaml(fields map[string]interface{}) ([]SkipFile, error) { - rawSkipFiles, err := unmarshalListOfFields(fields, "skip_files") +func UnmarshalSkipFilesFromBoilerplateConfigYaml(fields map[string]interface{}, fieldName string) ([]SkipFile, error) { + rawSkipFiles, err := unmarshalListOfFields(fields, fieldName) if err != nil || rawSkipFiles == nil { return nil, err }