From a6b61339097b886b6b4b95189815c278dd215ed7 Mon Sep 17 00:00:00 2001 From: Miguel Mendoza Date: Thu, 24 Oct 2024 22:36:26 -0700 Subject: [PATCH] feat: add support for nested template rendering Up to now, you could only templetize the main file, or define helper functions. This change makes it so that you can render any arbitrary file like this: {{ include "./path/to/file.yaml" . }} This is useful for dynamically generating policies without having to in-line them in the main template. --- cmd/apigee-go-gen/main.go | 3 + cmd/apigee-go-gen/render/apiproxy/cmd.go | 8 +- cmd/apigee-go-gen/render/sharedflow/cmd.go | 8 +- docs/render/commands/render-apiproxy.md | 3 +- docs/render/commands/render-sharedflow.md | 3 +- docs/render/using-built-in-helpers.md | 12 +- docs/render/using-openapi-spec.md | 11 +- examples/templates/oas3/apiproxy.yaml | 5 +- examples/templates/oas3/policies.yaml | 14 +-- .../oas3/policies/message-logging.yaml | 23 ++++ .../templates/oas3/policies/spike-arrest.yaml | 25 ++++ pkg/common/resources/helper_functions.txt | 15 ++- pkg/render/model.go | 12 +- pkg/render/render.go | 75 ++++++++---- pkg/render/render_test.go | 115 ++++++++++++++++++ pkg/render/testdata/.gitignore | 15 +++ .../testdata/render/policies/apiproxy.yaml | 19 +++ .../policies/common/assign-message.yaml | 29 +++++ .../render/policies/common/spike-arrest.yaml | 22 ++++ .../render/policies/exp-apiproxy.yaml | 44 +++++++ .../testdata/render/policies/policies.yaml | 15 +++ .../testdata/render/policies/values.yaml | 17 +++ .../render/using-files/exp-input.yaml | 15 +++ .../testdata/render/using-files/input.yaml | 15 +++ .../render/using-files/level1/input.yaml | 1 + .../using-files/level1/level2/input.yaml | 1 + .../render/using-helpers/_helpers.tmpl | 23 ++++ .../render/using-helpers/exp-input.yaml | 15 +++ .../testdata/render/using-helpers/input.yaml | 15 +++ pkg/utils/utils.go | 7 ++ pkg/utils/xml_test.go | 7 -- 31 files changed, 537 insertions(+), 55 deletions(-) create mode 100644 examples/templates/oas3/policies/message-logging.yaml create mode 100644 examples/templates/oas3/policies/spike-arrest.yaml create mode 100644 pkg/render/render_test.go create mode 100644 pkg/render/testdata/.gitignore create mode 100644 pkg/render/testdata/render/policies/apiproxy.yaml create mode 100644 pkg/render/testdata/render/policies/common/assign-message.yaml create mode 100644 pkg/render/testdata/render/policies/common/spike-arrest.yaml create mode 100755 pkg/render/testdata/render/policies/exp-apiproxy.yaml create mode 100644 pkg/render/testdata/render/policies/policies.yaml create mode 100644 pkg/render/testdata/render/policies/values.yaml create mode 100644 pkg/render/testdata/render/using-files/exp-input.yaml create mode 100644 pkg/render/testdata/render/using-files/input.yaml create mode 100644 pkg/render/testdata/render/using-files/level1/input.yaml create mode 100644 pkg/render/testdata/render/using-files/level1/level2/input.yaml create mode 100644 pkg/render/testdata/render/using-helpers/_helpers.tmpl create mode 100644 pkg/render/testdata/render/using-helpers/exp-input.yaml create mode 100644 pkg/render/testdata/render/using-helpers/input.yaml diff --git a/cmd/apigee-go-gen/main.go b/cmd/apigee-go-gen/main.go index 0e67715..768485f 100644 --- a/cmd/apigee-go-gen/main.go +++ b/cmd/apigee-go-gen/main.go @@ -37,6 +37,9 @@ func main() { } if showStack { + if isMultiErrors { + err = multiErrors.Errors[0] + } _, _ = fmt.Fprintf(os.Stderr, "%s\n", errors.Wrap(err, 0).Stack()) } os.Exit(1) diff --git a/cmd/apigee-go-gen/render/apiproxy/cmd.go b/cmd/apigee-go-gen/render/apiproxy/cmd.go index 0c82521..43e3e86 100644 --- a/cmd/apigee-go-gen/render/apiproxy/cmd.go +++ b/cmd/apigee-go-gen/render/apiproxy/cmd.go @@ -26,6 +26,7 @@ import ( ) var cFlags = render.NewCommonFlags() +var debug = flags.NewBool(false) var dryRun = flags.NewEnum([]string{"xml", "yaml"}) var validate = flags.NewBool(true) var setValue = flags.NewSetAny(cFlags.Values) @@ -42,7 +43,7 @@ var Cmd = &cobra.Command{ Short: "Generate an API proxy bundle from a template", Long: Usage(), RunE: func(cmd *cobra.Command, args []string) error { - if strings.TrimSpace(string(cFlags.OutputFile)) == "" && dryRun.IsUnset() { + if strings.TrimSpace(string(cFlags.OutputFile)) == "" && dryRun.IsUnset() && bool(debug) == false { return errors.New("required flag(s) \"output\" not set") } @@ -50,7 +51,7 @@ var Cmd = &cobra.Command{ return v1.NewAPIProxyModel(input) } - return render.GenerateBundle(createModelFunc, cFlags, bool(validate), dryRun.Value) + return render.GenerateBundle(createModelFunc, cFlags, bool(validate), dryRun.Value, bool(debug)) }, } @@ -59,7 +60,8 @@ func init() { Cmd.Flags().VarP(&cFlags.TemplateFile, "template", "t", `path to main template"`) Cmd.Flags().VarP(&cFlags.IncludeList, "include", "i", `path to helper templates (globs allowed)`) Cmd.Flags().VarP(&cFlags.OutputFile, "output", "o", `output directory or file`) - Cmd.Flags().VarP(&dryRun, "dry-run", "d", `prints rendered API proxy template to stdout"`) + Cmd.Flags().VarP(&debug, "debug", "", `prints rendered template before transforming into API proxy"`) + Cmd.Flags().VarP(&dryRun, "dry-run", "d", `prints rendered template after transforming into API Proxy"`) Cmd.Flags().VarP(&validate, "validate", "v", "check for unknown elements") Cmd.Flags().Var(&setValue, "set", `sets a key=value (bool,float,string), e.g. "use_ssl=true"`) Cmd.Flags().Var(&setValueStr, "set-string", `sets key=value (string), e.g. "base_path=/v1/hello" `) diff --git a/cmd/apigee-go-gen/render/sharedflow/cmd.go b/cmd/apigee-go-gen/render/sharedflow/cmd.go index 66f4791..28fb83b 100644 --- a/cmd/apigee-go-gen/render/sharedflow/cmd.go +++ b/cmd/apigee-go-gen/render/sharedflow/cmd.go @@ -26,6 +26,7 @@ import ( ) var cFlags = render.NewCommonFlags() +var debug = flags.NewBool(false) var dryRun = flags.NewEnum([]string{"xml", "yaml"}) var validate = flags.NewBool(true) var setValue = flags.NewSetAny(cFlags.Values) @@ -42,7 +43,7 @@ var Cmd = &cobra.Command{ Short: "Generate a shared flow bundle from a template", Long: Usage(), RunE: func(cmd *cobra.Command, args []string) error { - if strings.TrimSpace(string(cFlags.OutputFile)) == "" && dryRun.IsUnset() { + if strings.TrimSpace(string(cFlags.OutputFile)) == "" && dryRun.IsUnset() && bool(debug) == false { return errors.New("required flag(s) \"output\" not set") } @@ -50,7 +51,7 @@ var Cmd = &cobra.Command{ return v1.NewSharedFlowBundleModel(input) } - return render.GenerateBundle(createModelFunc, cFlags, bool(validate), dryRun.Value) + return render.GenerateBundle(createModelFunc, cFlags, bool(validate), dryRun.Value, bool(debug)) }, } @@ -59,7 +60,8 @@ func init() { Cmd.Flags().VarP(&cFlags.TemplateFile, "template", "t", `path to main template"`) Cmd.Flags().VarP(&cFlags.IncludeList, "include", "i", `path to helper templates (globs allowed)`) Cmd.Flags().VarP(&cFlags.OutputFile, "output", "o", `output directory or file`) - Cmd.Flags().VarP(&dryRun, "dry-run", "d", `prints rendered template to stdout"`) + Cmd.Flags().VarP(&debug, "debug", "", `prints rendered template before transforming into shared flow"`) + Cmd.Flags().VarP(&dryRun, "dry-run", "d", `prints rendered template after transforming into shared flow"`) Cmd.Flags().VarP(&validate, "validate", "v", "check for unknown elements") Cmd.Flags().Var(&setValue, "set", `sets a key=value (bool,float,string), e.g. "use_ssl=true"`) Cmd.Flags().Var(&setValueStr, "set-string", `sets key=value (string), e.g. "base_path=/v1/hello" `) diff --git a/docs/render/commands/render-apiproxy.md b/docs/render/commands/render-apiproxy.md index eb3cce5..5a7a8c5 100644 --- a/docs/render/commands/render-apiproxy.md +++ b/docs/render/commands/render-apiproxy.md @@ -39,7 +39,8 @@ The `render apiproxy` command takes the following parameters: -t, --template string path to main template" -i, --include string path to helper templates (globs allowed) -o, --output string output directory or file - -d, --dry-run enum(xml|yaml) prints rendered API proxy template to stdout" + --debug boolean prints rendered template before transforming into API proxy" + -d, --dry-run enum(xml|yaml) prints rendered template after transforming into API Proxy" -v, --validate boolean check for unknown elements --set string sets a key=value (bool,float,string), e.g. "use_ssl=true" --set-string string sets key=value (string), e.g. "base_path=/v1/hello" diff --git a/docs/render/commands/render-sharedflow.md b/docs/render/commands/render-sharedflow.md index 545e419..7f35289 100644 --- a/docs/render/commands/render-sharedflow.md +++ b/docs/render/commands/render-sharedflow.md @@ -31,7 +31,8 @@ The `render sharedflow` command takes the following parameters: -t, --template string path to main template" -i, --include string path to helper templates (globs allowed) -o, --output string output directory or file - -d, --dry-run enum(xml|yaml) prints rendered template to stdout" + --debug boolean prints rendered template before transforming into shared flow" + -d, --dry-run enum(xml|yaml) prints rendered template after transforming into shared flow" -v, --validate boolean check for unknown elements --set string sets a key=value (bool,float,string), e.g. "use_ssl=true" --set-string string sets key=value (string), e.g. "base_path=/v1/hello" diff --git a/docs/render/using-built-in-helpers.md b/docs/render/using-built-in-helpers.md index 7f122eb..b5b5b99 100644 --- a/docs/render/using-built-in-helpers.md +++ b/docs/render/using-built-in-helpers.md @@ -24,7 +24,7 @@ The template rendering commands include a set of built-in helper functions to as func include(template string, data any) string ``` -This function allows you to invoke your own [custom helper functions](./using-custom-helpers.md) +This function allows you to invoke your own [custom helper functions](./using-custom-helpers.md). e.g. @@ -32,6 +32,16 @@ e.g. {{ include "sayHello" $data }} ``` +This function can also be used to render a file. + +e.g. + +```shell +{{ include "./path/to/file.yaml" . }} +``` + +> The path to template file to render is relative to the parent template file. + ### **os_writefile** ```go diff --git a/docs/render/using-openapi-spec.md b/docs/render/using-openapi-spec.md index c20e827..6cdeaba 100644 --- a/docs/render/using-openapi-spec.md +++ b/docs/render/using-openapi-spec.md @@ -56,11 +56,18 @@ apigee-go-gen render apiproxy \ --output ./out/apiproxies/petstore ``` -## Dry run +## Dry run / Debug For rapid development, you can print the rendered template directly to stdout in your terminal. -Add the `--dry-run xml` or `--dry-run yaml` flag. e.g. +Add the `--dry-run xml` or `--dry-run yaml` flag. + +Note that dry-run is only useful when the rendered template produces valid YAML. + +If your template has issues, and it does not produce valid YAML, you can use the `--debug true` flag. + +This will print out the rendered template before even attempting to parse it as YAML. + === "XML output" ```shell diff --git a/examples/templates/oas3/apiproxy.yaml b/examples/templates/oas3/apiproxy.yaml index 7d58bf2..2c3980a 100755 --- a/examples/templates/oas3/apiproxy.yaml +++ b/examples/templates/oas3/apiproxy.yaml @@ -18,14 +18,15 @@ APIProxy: DisplayName: {{ $.Values.spec.info.title }} Description: |- {{ $.Values.spec.info.description | nindent 4 }} -Policies: - $ref: ./policies.yaml#/ +Policies: {{ include "./policies.yaml" . | nindent 2 }} ProxyEndpoints: - ProxyEndpoint: .name: default PreFlow: .name: PreFlow Request: + - Step: + Name: Spike-Arrest - Step: Name: OAS-Validate Flows: diff --git a/examples/templates/oas3/policies.yaml b/examples/templates/oas3/policies.yaml index d407f4b..e56a317 100644 --- a/examples/templates/oas3/policies.yaml +++ b/examples/templates/oas3/policies.yaml @@ -11,7 +11,7 @@ # 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. ---- + - OASValidation: .continueOnError: false .enabled: true @@ -37,13 +37,5 @@ StatusCode: 404 ReasonPhrase: Not found IgnoreUnresolvedVariables: true -- MessageLogging: - .name: ML-Logging-OK - Syslog: - Message: '[3f509b58 tag="{organization.name}.{apiproxy.name}.{environment.name}"] Weather request for WOEID {request.queryparam.w}.' - Host: example.loggly.com - Port: 514 - Protocol: TCP - FormatMessage: true - DateFormat: yyMMdd-HH:mm:ss.SSS - logLevel: ALERT +- {{ include "./policies/message-logging.yaml" . | indent 2 | trim }} +- {{ include "./policies/spike-arrest.yaml" . | indent 2 | trim }} \ No newline at end of file diff --git a/examples/templates/oas3/policies/message-logging.yaml b/examples/templates/oas3/policies/message-logging.yaml new file mode 100644 index 0000000..416adcd --- /dev/null +++ b/examples/templates/oas3/policies/message-logging.yaml @@ -0,0 +1,23 @@ +# Copyright 2024 Google LLC +# +# 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. +MessageLogging: + .name: ML-Logging-OK + Syslog: + Message: '[3f509b58 tag="{organization.name}.{apiproxy.name}.{environment.name}"] Weather request for WOEID {request.queryparam.w}.' + Host: example.loggly.com + Port: 514 + Protocol: TCP + FormatMessage: true + DateFormat: yyMMdd-HH:mm:ss.SSS + logLevel: ALERT \ No newline at end of file diff --git a/examples/templates/oas3/policies/spike-arrest.yaml b/examples/templates/oas3/policies/spike-arrest.yaml new file mode 100644 index 0000000..1a64e71 --- /dev/null +++ b/examples/templates/oas3/policies/spike-arrest.yaml @@ -0,0 +1,25 @@ +# Copyright 2024 Google LLC +# +# 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. +SpikeArrest: + .continueOnError: false + .enabled: true + .name: Spike-Arrest + DisplayName: Spike Arrest + Properties: {} + Identifier: + .ref: client.ip + # The example below sets the Rate value dynamically from the render context + # You can pass the value like this --set spike_arrest_rate=300pm in the command line + # If the value is unset, it defaults to 100pm + Rate: {{ or ($.Values.spike_arrest_rate) "100pm" }} \ No newline at end of file diff --git a/pkg/common/resources/helper_functions.txt b/pkg/common/resources/helper_functions.txt index b07bf6b..de72638 100644 --- a/pkg/common/resources/helper_functions.txt +++ b/pkg/common/resources/helper_functions.txt @@ -1,5 +1,18 @@ include(template string, data any) - Invoke an existing template e.g {{ include "sayHello" $data }} + Render a pre-defined template, e.g. + + {{ include "greet" "Miguel" }} + + The template must be defined ahead of time with a "define" block, e.g. + + {{- define "greet" -}} + {{- $name := . -}} + Hello {{ $name }} ! + {{- end -}} + + You can also use this function to render an existing file. e.g. + + {{ include "./path/to/file.yaml" . }} os_writefile(dest string, content string) Write a file to the output directory diff --git a/pkg/render/model.go b/pkg/render/model.go index a067f66..d5a7120 100644 --- a/pkg/render/model.go +++ b/pkg/render/model.go @@ -25,7 +25,7 @@ import ( "path/filepath" ) -func GenerateBundle(createModelFunc func(string) (v1.Model, error), cFlags *CommonFlags, validate bool, dryRun string) error { +func GenerateBundle(createModelFunc func(string) (v1.Model, error), cFlags *CommonFlags, validate bool, dryRun string, debug bool) error { var err error bundleOutputFile := cFlags.OutputFile @@ -51,9 +51,17 @@ func GenerateBundle(createModelFunc func(string) (v1.Model, error), cFlags *Comm return errors.New(err) } + if debug { + fmt.Println(string(rendered)) + return nil + } + rendered, err = ResolveYAML(rendered, templateFile) if err != nil { - return err + return utils.MultiError{ + Errors: []error{ + err, + errors.New("rendered template appears to not be valid YAML. Use --debug=true flag to inspect rendered output")}} } err = os.WriteFile(string(cFlags.OutputFile), rendered, os.ModePerm) diff --git a/pkg/render/render.go b/pkg/render/render.go index 8010663..bc9a7b5 100644 --- a/pkg/render/render.go +++ b/pkg/render/render.go @@ -19,7 +19,6 @@ import ( "fmt" "github.com/Masterminds/sprig/v3" "github.com/apigee/apigee-go-gen/pkg/flags" - "github.com/apigee/apigee-go-gen/pkg/utils" "github.com/bmatcuk/doublestar/v4" "github.com/go-errors/errors" "github.com/gosimple/slug" @@ -29,6 +28,7 @@ import ( "path/filepath" "reflect" "regexp" + "slices" "strings" "text/template" ) @@ -100,20 +100,6 @@ func CreateTemplate(templateFile string, includeList []string, outputFile string return nil, err } - //make a copy of the template to create a unique temporary main file - tmpFile, err := os.CreateTemp("", "tpl_*_"+filepath.Base(templateFile)) - if err != nil { - return nil, errors.New(err) - } - defer func() { utils.MustClose(tmpFile) }() - - err = utils.CopyFile(tmpFile.Name(), templateFile) - if err != nil { - return nil, err - } - - includeMatches = append([]string{tmpFile.Name()}, includeMatches...) - helperFuncs := map[string]any{} blankFunc := func(args ...any) string { @@ -209,23 +195,55 @@ func CreateTemplate(templateFile string, includeList []string, outputFile string return "" } + includeStack := []string{fmt.Sprintf("file:%s", templateFile)} + includeFunc := func(args ...any) string { if len(args) < 0 { panic("include function requires at least one argument") } - templateName := args[0].(string) - templateText := fmt.Sprintf(`{{- template "%s" . }}`, templateName) + arg0 := args[0].(string) + + var err error + var templateBytes []byte + var templateName string + + parentTemplateIndex := slices.IndexFunc(includeStack, func(elem string) bool { + return strings.Index(elem, "file:") == 0 + }) + + parentTemplateFile := strings.TrimPrefix(includeStack[parentTemplateIndex], "file:") + targetTemplateFile := filepath.Join(filepath.Dir(parentTemplateFile), arg0) + + if templateBytes, err = os.ReadFile(targetTemplateFile); err == nil { + // file was found, use its contents as template + templateName = arg0 + includeStack = slices.Insert(includeStack, 0, fmt.Sprintf("file:%s", targetTemplateFile)) + + } else { + //no such file, assume it's a named template + templateBytes = []byte(fmt.Sprintf(`{{- template "%s" . }}`, arg0)) + templateName = fmt.Sprintf("%s.tpl", arg0) + includeStack = slices.Insert(includeStack, 0, fmt.Sprintf("template:%s", templateName)) + } + + templateText := string(templateBytes) - tpl, _ := template.New(templateName + ".tpl"). + tpl, err := template.New(templateName). Funcs(helperFuncs). Funcs(sprig.FuncMap()). Parse(templateText) - tpl, err := tpl.ParseFiles(includeMatches...) if err != nil { panic(err) } + if len(includeMatches) > 0 { + tpl, err = tpl.ParseFiles(includeMatches...) + if err != nil { + panic(err) + } + } + var arg any if len(args) > 1 { //actual argument @@ -239,6 +257,9 @@ func CreateTemplate(templateFile string, includeList []string, outputFile string if err != nil { panic(err) } + + includeStack = slices.Delete(includeStack, 0, 1) + return tplOut.String() } @@ -254,14 +275,26 @@ func CreateTemplate(templateFile string, includeList []string, outputFile string helperFuncs["deref"] = derefFunc helperFuncs["slug_make"] = slugMakeFunc - tmpl, err := template.New(filepath.Base(tmpFile.Name())). + var templateText []byte + if templateText, err = os.ReadFile(templateFile); err != nil { + return nil, errors.New(err) + } + + tmpl, err := template.New(templateFile). Funcs(helperFuncs). Funcs(sprig.FuncMap()). - ParseFiles(includeMatches...) + Parse(string(templateText)) if err != nil { return nil, errors.New(err) } + if len(includeMatches) > 0 { + tmpl, err = tmpl.ParseFiles(includeMatches...) + if err != nil { + return nil, errors.New(err) + } + } + return tmpl, nil } diff --git a/pkg/render/render_test.go b/pkg/render/render_test.go new file mode 100644 index 0000000..a876794 --- /dev/null +++ b/pkg/render/render_test.go @@ -0,0 +1,115 @@ +// Copyright 2024 Google LLC +// +// 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 render + +import ( + "fmt" + "github.com/apigee/apigee-go-gen/pkg/flags" + "github.com/apigee/apigee-go-gen/pkg/utils" + "github.com/stretchr/testify/require" + "os" + "path" + "path/filepath" + "testing" +) + +func TestRenderGeneric(t *testing.T) { + + tests := []struct { + dir string + templateFile string + valuesFile string + includesFile string + wantErr error + }{ + { + "using-files", + "input.yaml", + "", + "", + nil, + }, + { + "using-helpers", + "input.yaml", + "", + "_helpers.tmpl", + nil, + }, + { + "policies", + "apiproxy.yaml", + "values.yaml", + "", + nil, + }, + } + for _, tt := range tests { + t.Run(tt.dir, func(t *testing.T) { + + testDir := path.Join("testdata", "render", tt.dir) + templateFile := path.Join(testDir, tt.templateFile) + + outputFile := path.Join(testDir, fmt.Sprintf("out-%s", tt.templateFile)) + expectedFile := path.Join(testDir, fmt.Sprintf("exp-%s", tt.templateFile)) + + var err error + err = os.RemoveAll(outputFile) + require.NoError(t, err) + + type ctx struct{} + cFlags := NewCommonFlags() + cFlags.TemplateFile = flags.String(templateFile) + cFlags.OutputFile = flags.String(outputFile) + + if tt.valuesFile != "" { + v := flags.NewValues(cFlags.Values) + valuesFile := path.Join(testDir, tt.valuesFile) + err := v.Set(valuesFile) + require.NoError(t, err) + } + + if tt.includesFile != "" { + includesFile := path.Join(testDir, tt.includesFile) + err := cFlags.IncludeList.Set(includesFile) + require.NoError(t, err) + } + + err = RenderGenericTemplate(cFlags, false) + + if tt.wantErr != nil { + require.EqualError(t, err, tt.wantErr.Error()) + return + } + require.NoError(t, err) + + outputBytes := utils.MustReadFileBytes(outputFile) + expectedBytes := utils.MustReadFileBytes(expectedFile) + + if filepath.Ext(expectedFile) == ".txt" { + require.Equal(t, string(expectedBytes), string(outputBytes)) + } else if filepath.Ext(expectedFile) == ".json" { + require.JSONEq(t, string(expectedBytes), string(outputBytes)) + } else if filepath.Ext(expectedFile) == ".yaml" { + outputBytes = utils.RemoveYAMLComments(outputBytes) + expectedBytes = utils.RemoveYAMLComments(expectedBytes) + require.YAMLEq(t, string(expectedBytes), string(outputBytes)) + } else { + t.Error("unknown output format in testcase") + } + + }) + } +} diff --git a/pkg/render/testdata/.gitignore b/pkg/render/testdata/.gitignore new file mode 100644 index 0000000..edfdbc3 --- /dev/null +++ b/pkg/render/testdata/.gitignore @@ -0,0 +1,15 @@ +# Copyright 2024 Google LLC +# +# 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. + +**/out-* \ No newline at end of file diff --git a/pkg/render/testdata/render/policies/apiproxy.yaml b/pkg/render/testdata/render/policies/apiproxy.yaml new file mode 100644 index 0000000..72652c9 --- /dev/null +++ b/pkg/render/testdata/render/policies/apiproxy.yaml @@ -0,0 +1,19 @@ +# Copyright 2024 Google LLC +# +# 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. +APIProxy: + .revision: 1 + .name: Example + DisplayName: Example + Description: Example +Policies: {{ include "policies.yaml" . | nindent 2 }} \ No newline at end of file diff --git a/pkg/render/testdata/render/policies/common/assign-message.yaml b/pkg/render/testdata/render/policies/common/assign-message.yaml new file mode 100644 index 0000000..ea91a7e --- /dev/null +++ b/pkg/render/testdata/render/policies/common/assign-message.yaml @@ -0,0 +1,29 @@ +# Copyright 2024 Google LLC +# +# 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. +AssignMessage: + .continueOnError: false + .enabled: true + .name: AM-SetHeader + DisplayName: AM-SetHeader + Properties: {} + Set: + Headers: + Header: + .name: Example + -Data: {{ $.Values.example_header }} + IgnoreUnresolvedVariables: true + AssignTo: + .createNew: false + .transport: http + .type: request \ No newline at end of file diff --git a/pkg/render/testdata/render/policies/common/spike-arrest.yaml b/pkg/render/testdata/render/policies/common/spike-arrest.yaml new file mode 100644 index 0000000..5039232 --- /dev/null +++ b/pkg/render/testdata/render/policies/common/spike-arrest.yaml @@ -0,0 +1,22 @@ +# Copyright 2024 Google LLC +# +# 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. +SpikeArrest: + .continueOnError: false + .enabled: true + .name: Spike-Arrest + DisplayName: Spike Arrest + Properties: {} + Identifier: + .ref: {{ $.Values.spike_arrest.identifier }} + Rate: {{ or ($.Values.spike_arrest.rate) "100pm" }} \ No newline at end of file diff --git a/pkg/render/testdata/render/policies/exp-apiproxy.yaml b/pkg/render/testdata/render/policies/exp-apiproxy.yaml new file mode 100755 index 0000000..c156721 --- /dev/null +++ b/pkg/render/testdata/render/policies/exp-apiproxy.yaml @@ -0,0 +1,44 @@ +# Copyright 2024 Google LLC +# +# 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. +APIProxy: + .revision: 1 + .name: Example + DisplayName: Example + Description: Example +Policies: + - SpikeArrest: + .continueOnError: false + .enabled: true + .name: Spike-Arrest + DisplayName: Spike Arrest + Properties: {} + Identifier: + .ref: client.ip + Rate: 400pm + - AssignMessage: + .continueOnError: false + .enabled: true + .name: AM-SetHeader + DisplayName: AM-SetHeader + Properties: {} + Set: + Headers: + Header: + .name: Example + -Data: example_value + IgnoreUnresolvedVariables: true + AssignTo: + .createNew: false + .transport: http + .type: request \ No newline at end of file diff --git a/pkg/render/testdata/render/policies/policies.yaml b/pkg/render/testdata/render/policies/policies.yaml new file mode 100644 index 0000000..bd62a7b --- /dev/null +++ b/pkg/render/testdata/render/policies/policies.yaml @@ -0,0 +1,15 @@ +# Copyright 2024 Google LLC +# +# 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. +- {{ include "./common/spike-arrest.yaml" . | indent 2 | trim }} +- {{ include "./common/assign-message.yaml" . | indent 2 | trim }} \ No newline at end of file diff --git a/pkg/render/testdata/render/policies/values.yaml b/pkg/render/testdata/render/policies/values.yaml new file mode 100644 index 0000000..f1a7718 --- /dev/null +++ b/pkg/render/testdata/render/policies/values.yaml @@ -0,0 +1,17 @@ +# Copyright 2024 Google LLC +# +# 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. +spike_arrest: + rate: 400pm + identifier: client.ip +example_header: example_value \ No newline at end of file diff --git a/pkg/render/testdata/render/using-files/exp-input.yaml b/pkg/render/testdata/render/using-files/exp-input.yaml new file mode 100644 index 0000000..4b6c586 --- /dev/null +++ b/pkg/render/testdata/render/using-files/exp-input.yaml @@ -0,0 +1,15 @@ +# Copyright 2024 Google LLC +# +# 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. + +The quick brown fox jumped over the lazy dog \ No newline at end of file diff --git a/pkg/render/testdata/render/using-files/input.yaml b/pkg/render/testdata/render/using-files/input.yaml new file mode 100644 index 0000000..a9760fd --- /dev/null +++ b/pkg/render/testdata/render/using-files/input.yaml @@ -0,0 +1,15 @@ +# Copyright 2024 Google LLC +# +# 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. + +The quick brown {{ include "./level1/input.yaml" }} \ No newline at end of file diff --git a/pkg/render/testdata/render/using-files/level1/input.yaml b/pkg/render/testdata/render/using-files/level1/input.yaml new file mode 100644 index 0000000..8202b59 --- /dev/null +++ b/pkg/render/testdata/render/using-files/level1/input.yaml @@ -0,0 +1 @@ +fox jumped over {{ include "./level2/input.yaml" }} \ No newline at end of file diff --git a/pkg/render/testdata/render/using-files/level1/level2/input.yaml b/pkg/render/testdata/render/using-files/level1/level2/input.yaml new file mode 100644 index 0000000..ae8968f --- /dev/null +++ b/pkg/render/testdata/render/using-files/level1/level2/input.yaml @@ -0,0 +1 @@ +the lazy dog \ No newline at end of file diff --git a/pkg/render/testdata/render/using-helpers/_helpers.tmpl b/pkg/render/testdata/render/using-helpers/_helpers.tmpl new file mode 100644 index 0000000..3104763 --- /dev/null +++ b/pkg/render/testdata/render/using-helpers/_helpers.tmpl @@ -0,0 +1,23 @@ +{{/* + Copyright 2024 Google LLC + + 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. +*/}} + +{{- define "level1" -}} + fox jumped over {{ include "level2" }} +{{- end -}} + +{{- define "level2" -}} + the lazy dog +{{- end -}} \ No newline at end of file diff --git a/pkg/render/testdata/render/using-helpers/exp-input.yaml b/pkg/render/testdata/render/using-helpers/exp-input.yaml new file mode 100644 index 0000000..4b6c586 --- /dev/null +++ b/pkg/render/testdata/render/using-helpers/exp-input.yaml @@ -0,0 +1,15 @@ +# Copyright 2024 Google LLC +# +# 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. + +The quick brown fox jumped over the lazy dog \ No newline at end of file diff --git a/pkg/render/testdata/render/using-helpers/input.yaml b/pkg/render/testdata/render/using-helpers/input.yaml new file mode 100644 index 0000000..7d5af0c --- /dev/null +++ b/pkg/render/testdata/render/using-helpers/input.yaml @@ -0,0 +1,15 @@ +# Copyright 2024 Google LLC +# +# 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. + +The quick brown {{ include "level1" . }} \ No newline at end of file diff --git a/pkg/utils/utils.go b/pkg/utils/utils.go index 2ac732e..2363735 100644 --- a/pkg/utils/utils.go +++ b/pkg/utils/utils.go @@ -20,6 +20,7 @@ import ( "github.com/go-errors/errors" "gopkg.in/yaml.v3" "os" + "regexp" ) func MustReadFileBytes(path string) []byte { @@ -84,3 +85,9 @@ func PushDir(dir string) func() { return popDir } + +func RemoveYAMLComments(data []byte) []byte { + regex := regexp.MustCompile(`(?ms)^\s*#[^\n\r]*$[\r\n]*`) + replaced := regex.ReplaceAll(data, []byte{}) + return replaced +} diff --git a/pkg/utils/xml_test.go b/pkg/utils/xml_test.go index c0a434e..b066201 100644 --- a/pkg/utils/xml_test.go +++ b/pkg/utils/xml_test.go @@ -18,7 +18,6 @@ import ( "bytes" "github.com/stretchr/testify/assert" "path/filepath" - "regexp" "testing" ) @@ -83,9 +82,3 @@ func TestXMLText2YAMLText(t *testing.T) { }) } } - -func RemoveYAMLComments(data []byte) []byte { - regex := regexp.MustCompile(`(?ms)^\s*#[^\n\r]*$[\r\n]*`) - replaced := regex.ReplaceAll(data, []byte{}) - return replaced -}