Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Go template with sprig for application template render in applicationset #9873

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions applicationset/controllers/applicationset_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -429,8 +429,16 @@ func (r *ApplicationSetReconciler) generateApplications(applicationSetInfo argop
var firstError error
var applicationSetReason argoprojiov1alpha1.ApplicationSetReasonType

if (applicationSetInfo.Spec.Template == nil && applicationSetInfo.Spec.UntypedTemplate == nil) ||
(applicationSetInfo.Spec.Template != nil && applicationSetInfo.Spec.UntypedTemplate != nil) {
firstError = fmt.Errorf("application set spec should have either template or untypedTemplate defined")
applicationSetReason = argoprojiov1alpha1.ApplicationSetReasonErrorOccurred

return res, applicationSetReason, firstError
}

for _, requestedGenerator := range applicationSetInfo.Spec.Generators {
t, err := generators.Transform(requestedGenerator, r.Generators, applicationSetInfo.Spec.Template, &applicationSetInfo, map[string]string{})
t, err := generators.Transform(requestedGenerator, r.Generators, *applicationSetInfo.Spec.Template, &applicationSetInfo, map[string]string{})
if err != nil {
log.WithError(err).WithField("generator", requestedGenerator).
Error("error generating application from params")
Expand All @@ -445,7 +453,7 @@ func (r *ApplicationSetReconciler) generateApplications(applicationSetInfo argop
tmplApplication := getTempApplication(a.Template)

for _, p := range a.Params {
app, err := r.Renderer.RenderTemplateParams(tmplApplication, applicationSetInfo.Spec.SyncPolicy, p)
app, err := r.Renderer.RenderTemplateParams(tmplApplication, applicationSetInfo.Spec.UntypedTemplate, applicationSetInfo.Spec.SyncPolicy, p)
if err != nil {
log.WithError(err).WithField("params", a.Params).WithField("generator", requestedGenerator).
Error("error generating application from params")
Expand Down
34 changes: 17 additions & 17 deletions applicationset/controllers/applicationset_controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ func (g *generatorMock) GetRequeueAfter(appSetGenerator *argoprojiov1alpha1.Appl
return args.Get(0).(time.Duration)
}

func (r *rendererMock) RenderTemplateParams(tmpl *argov1alpha1.Application, syncPolicy *argoprojiov1alpha1.ApplicationSetSyncPolicy, params map[string]string) (*argov1alpha1.Application, error) {
func (r *rendererMock) RenderTemplateParams(tmpl *argov1alpha1.Application, untypedTemplate *argoprojiov1alpha1.ApplicationSetUntypedTemplate, syncPolicy *argoprojiov1alpha1.ApplicationSetSyncPolicy, params map[string]string) (*argov1alpha1.Application, error) {
args := r.Called(tmpl, params)

if args.Error(1) != nil {
Expand Down Expand Up @@ -189,7 +189,7 @@ func TestExtractApplications(t *testing.T) {
},
Spec: argoprojiov1alpha1.ApplicationSetSpec{
Generators: []argoprojiov1alpha1.ApplicationSetGenerator{generator},
Template: cc.template,
Template: &cc.template,
},
})

Expand Down Expand Up @@ -302,7 +302,7 @@ func TestMergeTemplateApplications(t *testing.T) {
},
Spec: argoprojiov1alpha1.ApplicationSetSpec{
Generators: []argoprojiov1alpha1.ApplicationSetGenerator{generator},
Template: cc.template,
Template: &cc.template,
},
},
)
Expand Down Expand Up @@ -372,7 +372,7 @@ func TestCreateOrUpdateInCluster(t *testing.T) {
Namespace: "namespace",
},
Spec: argoprojiov1alpha1.ApplicationSetSpec{
Template: argoprojiov1alpha1.ApplicationSetTemplate{
Template: &argoprojiov1alpha1.ApplicationSetTemplate{
Spec: argov1alpha1.ApplicationSpec{
Project: "project",
},
Expand Down Expand Up @@ -430,7 +430,7 @@ func TestCreateOrUpdateInCluster(t *testing.T) {
Namespace: "namespace",
},
Spec: argoprojiov1alpha1.ApplicationSetSpec{
Template: argoprojiov1alpha1.ApplicationSetTemplate{
Template: &argoprojiov1alpha1.ApplicationSetTemplate{
Spec: argov1alpha1.ApplicationSpec{
Project: "project",
},
Expand Down Expand Up @@ -488,7 +488,7 @@ func TestCreateOrUpdateInCluster(t *testing.T) {
Namespace: "namespace",
},
Spec: argoprojiov1alpha1.ApplicationSetSpec{
Template: argoprojiov1alpha1.ApplicationSetTemplate{
Template: &argoprojiov1alpha1.ApplicationSetTemplate{
Spec: argov1alpha1.ApplicationSpec{
Project: "project",
},
Expand Down Expand Up @@ -550,7 +550,7 @@ func TestCreateOrUpdateInCluster(t *testing.T) {
Namespace: "namespace",
},
Spec: argoprojiov1alpha1.ApplicationSetSpec{
Template: argoprojiov1alpha1.ApplicationSetTemplate{
Template: &argoprojiov1alpha1.ApplicationSetTemplate{
Spec: argov1alpha1.ApplicationSpec{
Project: "project",
},
Expand Down Expand Up @@ -610,7 +610,7 @@ func TestCreateOrUpdateInCluster(t *testing.T) {
Namespace: "namespace",
},
Spec: argoprojiov1alpha1.ApplicationSetSpec{
Template: argoprojiov1alpha1.ApplicationSetTemplate{
Template: &argoprojiov1alpha1.ApplicationSetTemplate{
Spec: argov1alpha1.ApplicationSpec{
Project: "project",
},
Expand Down Expand Up @@ -682,7 +682,7 @@ func TestCreateOrUpdateInCluster(t *testing.T) {
Namespace: "namespace",
},
Spec: argoprojiov1alpha1.ApplicationSetSpec{
Template: argoprojiov1alpha1.ApplicationSetTemplate{
Template: &argoprojiov1alpha1.ApplicationSetTemplate{
Spec: argov1alpha1.ApplicationSpec{
Project: "project",
Source: argov1alpha1.ApplicationSource{Path: "path", TargetRevision: "revision", RepoURL: "repoURL"},
Expand Down Expand Up @@ -762,7 +762,7 @@ func TestCreateOrUpdateInCluster(t *testing.T) {
Namespace: "namespace",
},
Spec: argoprojiov1alpha1.ApplicationSetSpec{
Template: argoprojiov1alpha1.ApplicationSetTemplate{
Template: &argoprojiov1alpha1.ApplicationSetTemplate{
Spec: argov1alpha1.ApplicationSpec{
Project: "project",
},
Expand Down Expand Up @@ -902,7 +902,7 @@ func TestRemoveFinalizerOnInvalidDestination_FinalizerTypes(t *testing.T) {
Namespace: "namespace",
},
Spec: argoprojiov1alpha1.ApplicationSetSpec{
Template: argoprojiov1alpha1.ApplicationSetTemplate{
Template: &argoprojiov1alpha1.ApplicationSetTemplate{
Spec: argov1alpha1.ApplicationSpec{
Project: "project",
},
Expand Down Expand Up @@ -1064,7 +1064,7 @@ func TestRemoveFinalizerOnInvalidDestination_DestinationTypes(t *testing.T) {
Namespace: "namespace",
},
Spec: argoprojiov1alpha1.ApplicationSetSpec{
Template: argoprojiov1alpha1.ApplicationSetTemplate{
Template: &argoprojiov1alpha1.ApplicationSetTemplate{
Spec: argov1alpha1.ApplicationSpec{
Project: "project",
},
Expand Down Expand Up @@ -1192,7 +1192,7 @@ func TestCreateApplications(t *testing.T) {
Namespace: "namespace",
},
Spec: argoprojiov1alpha1.ApplicationSetSpec{
Template: argoprojiov1alpha1.ApplicationSetTemplate{
Template: &argoprojiov1alpha1.ApplicationSetTemplate{
Spec: argov1alpha1.ApplicationSpec{
Project: "project",
},
Expand Down Expand Up @@ -1249,7 +1249,7 @@ func TestCreateApplications(t *testing.T) {
Namespace: "namespace",
},
Spec: argoprojiov1alpha1.ApplicationSetSpec{
Template: argoprojiov1alpha1.ApplicationSetTemplate{
Template: &argoprojiov1alpha1.ApplicationSetTemplate{
Spec: argov1alpha1.ApplicationSpec{
Project: "project",
},
Expand Down Expand Up @@ -1361,7 +1361,7 @@ func TestDeleteInCluster(t *testing.T) {
Namespace: "namespace",
},
Spec: argoprojiov1alpha1.ApplicationSetSpec{
Template: argoprojiov1alpha1.ApplicationSetTemplate{
Template: &argoprojiov1alpha1.ApplicationSetTemplate{
Spec: argov1alpha1.ApplicationSpec{
Project: "project",
},
Expand Down Expand Up @@ -1803,7 +1803,7 @@ func TestReconcilerValidationErrorBehaviour(t *testing.T) {
},
},
},
Template: argoprojiov1alpha1.ApplicationSetTemplate{
Template: &argoprojiov1alpha1.ApplicationSetTemplate{
ApplicationSetTemplateMeta: argoprojiov1alpha1.ApplicationSetTemplateMeta{
Name: "{{cluster}}",
Namespace: "argocd",
Expand Down Expand Up @@ -1889,7 +1889,7 @@ func TestSetApplicationSetStatusCondition(t *testing.T) {
}},
}},
},
Template: argoprojiov1alpha1.ApplicationSetTemplate{},
Template: &argoprojiov1alpha1.ApplicationSetTemplate{},
},
}

Expand Down
14 changes: 10 additions & 4 deletions applicationset/generators/generator_spec_processor.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@ package generators

import (
"encoding/json"
"reflect"

"github.com/argoproj/argo-cd/v2/applicationset/utils"
"github.com/valyala/fasttemplate"
"reflect"

argoprojiov1alpha1 "github.com/argoproj/argo-cd/v2/pkg/apis/applicationset/v1alpha1"
"github.com/imdario/mergo"
Expand All @@ -25,7 +26,7 @@ func Transform(requestedGenerator argoprojiov1alpha1.ApplicationSetGenerator, al
generators := GetRelevantGenerators(&requestedGenerator, allGenerators)
for _, g := range generators {
// we call mergeGeneratorTemplate first because GenerateParams might be more costly so we want to fail fast if there is an error
mergedTemplate, err := mergeGeneratorTemplate(g, &requestedGenerator, baseTemplate)
mergedTemplate, err := mergeGeneratorTemplate(g, &requestedGenerator, &baseTemplate)
if err != nil {
log.WithError(err).WithField("generator", g).
Error("error generating params")
Expand Down Expand Up @@ -84,14 +85,19 @@ func GetRelevantGenerators(requestedGenerator *argoprojiov1alpha1.ApplicationSet
return res
}

func mergeGeneratorTemplate(g Generator, requestedGenerator *argoprojiov1alpha1.ApplicationSetGenerator, applicationSetTemplate argoprojiov1alpha1.ApplicationSetTemplate) (argoprojiov1alpha1.ApplicationSetTemplate, error) {
func mergeGeneratorTemplate(g Generator, requestedGenerator *argoprojiov1alpha1.ApplicationSetGenerator, applicationSetTemplate *argoprojiov1alpha1.ApplicationSetTemplate) (argoprojiov1alpha1.ApplicationSetTemplate, error) {

// Make a copy of the value from `GetTemplate()` before merge, rather than copying directly into
// the provided parameter (which will touch the original resource object returned by client-go)
dest := g.GetTemplate(requestedGenerator).DeepCopy()

err := mergo.Merge(dest, applicationSetTemplate)
var err error

if applicationSetTemplate != nil {
err = mergo.Merge(dest, applicationSetTemplate)
} else {
log.Warn("generator template won't be applied when standard application template is not used")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this something we can't support? Seems that we could 1) render the untypedTemplate, 2) unmarshal the rendered template, 3) merge it with the generator template and 4) render the merged template.

}
return *dest, err
}

Expand Down
159 changes: 159 additions & 0 deletions applicationset/utils/templating.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
package utils

import (
"bytes"
"encoding/json"
"fmt"
"io"
"strconv"
"strings"
"text/template"

"github.com/Masterminds/sprig/v3"
argov1alpha1 "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1"
argoprojiov1alpha1 "github.com/argoproj/argo-cd/v2/pkg/apis/applicationset/v1alpha1"
"github.com/valyala/fasttemplate"
"sigs.k8s.io/yaml"
)

type Renderer interface {
RenderTemplateParams(tmpl *argov1alpha1.Application, untypedTemplate *argoprojiov1alpha1.ApplicationSetUntypedTemplate, syncPolicy *argoprojiov1alpha1.ApplicationSetSyncPolicy, params map[string]string) (*argov1alpha1.Application, error)
}
type Render struct {
}

func (r *Render) RenderTemplateParams(tmpl *argov1alpha1.Application, untypedTemplate *argoprojiov1alpha1.ApplicationSetUntypedTemplate, syncPolicy *argoprojiov1alpha1.ApplicationSetSyncPolicy, params map[string]string) (*argov1alpha1.Application, error) {
if tmpl == nil {
return nil, fmt.Errorf("application template is empty ")
}
if len(params) == 0 {
return tmpl, nil
}
var replacedTmpl argov1alpha1.Application
var replacedTmplStr string
if untypedTemplate == nil {
// interpolates the `${expression}` first and simple `{reference}` right after
tmplBytes, err := json.Marshal(tmpl)
if err != nil {
return nil, err
}
replacedTmplStr, err = renderWithFastTemplateAndGoTemplate(string(tmplBytes), params)
if err != nil {
return nil, err
}
replacedTmplStr = renderWithFastTemplate(replacedTmplStr, params)
if err != nil {
return nil, err
}
err = json.Unmarshal([]byte(replacedTmplStr), &replacedTmpl)
if err != nil {
return nil, err
}
} else {
replacedTmplStr, err := renderWithGoTemplate(string(*untypedTemplate), params)
if err != nil {
return nil, err
}
// UnmarshalStrict to fail early and raise the fact that template
// result produced not what is expected
err = yaml.UnmarshalStrict([]byte(replacedTmplStr), &replacedTmpl)
if err != nil {
return nil, err
}
}
// Add the 'resources-finalizer' finalizer if:
// The template application doesn't have any finalizers, and:
// a) there is no syncPolicy, or
// b) there IS a syncPolicy, but preserveResourcesOnDeletion is set to false
// See TestRenderTemplateParamsFinalizers in util_test.go for test-based definition of behaviour
if (syncPolicy == nil || !syncPolicy.PreserveResourcesOnDeletion) &&
(replacedTmpl.ObjectMeta.Finalizers == nil || len(replacedTmpl.ObjectMeta.Finalizers) == 0) {
replacedTmpl.ObjectMeta.Finalizers = []string{"resources-finalizer.argocd.argoproj.io"}
}
return &replacedTmpl, nil
}

// renderWithGoTemplate executes gotemplate with sprig functions against the raw (untyped) template
func renderWithGoTemplate(rawTemplate string, data map[string]string) (string, error) {
goTemplate, err := template.New("").Option("missingkey=zero").Funcs(createFuncMap()).Parse(rawTemplate)
if err != nil {
return "", err
}
var tplString bytes.Buffer
err = goTemplate.Execute(&tplString, data)
if err != nil {
return "", err
}
return tplString.String(), nil
}

// renderWithFastTemplateAndGoTemplate executes string substitution with the result of gotemplate run
// for every token found
func renderWithFastTemplateAndGoTemplate(rawTemplate string, data map[string]string) (string, error) {
fstTmpl := fasttemplate.New(rawTemplate, "${{", "}}")
replacedTmplStr, err := fstTmpl.ExecuteFuncStringWithErr(func(w io.Writer, tag string) (int, error) {
trimmedTag := strings.TrimSpace(tag)
// json control characters here are double escaped, what was a double quote " becomes \" when
// unmarshalled to ApplicationSet and then \\\" when marshaled to json
// this becomes a problem with gotemplate trying to parse the token, so unquote the string
unquotedTag, err := strconv.Unquote(`"` + trimmedTag + `"`)
if err != nil {
return 0, err
}
// wrapping back in {{}} for gotemplate to identify the expression
gotemplateTag := fmt.Sprintf("{{%s}}", unquotedTag)
goTemplate, err := template.New("").Option("missingkey=zero").Funcs(createFuncMap()).Parse(gotemplateTag)
if err != nil {
return 0, err
}
var tplString bytes.Buffer
err = goTemplate.Execute(&tplString, data)
if err != nil {
return 0, err
}
// The following escapes any special characters (e.g. newlines, tabs, etc...)
// in preparation for substitution
replacement := strconv.Quote(tplString.String())
replacement = replacement[1 : len(replacement)-1]
return w.Write([]byte(replacement))
})
if err != nil {
return "", err
}
return replacedTmplStr, nil
}

// replaceWithFastTemplate executes basic string substitution of a template with replacement values.
func renderWithFastTemplate(rawTemplate string, data map[string]string) string {
fstTmpl := fasttemplate.New(rawTemplate, "{{", "}}")
replacedTmplStr := fstTmpl.ExecuteFuncString(func(w io.Writer, tag string) (int, error) {
trimmedTag := strings.TrimSpace(tag)
replacement, ok := data[trimmedTag]
if len(trimmedTag) == 0 || !ok {
return w.Write([]byte(fmt.Sprintf("{{%s}}", tag)))
}
// The following escapes any special characters (e.g. newlines, tabs, etc...)
// in preparation for substitution
replacement = strconv.Quote(replacement)
replacement = replacement[1 : len(replacement)-1]
return w.Write([]byte(replacement))
})
return replacedTmplStr
}
func ToYaml(v interface{}) (string, error) {
data, err := yaml.Marshal(v)
if err != nil {
return "", err
}
return string(data), nil
}
func createFuncMap() template.FuncMap {
funcMap := sprig.TxtFuncMap()
extraFuncMap := template.FuncMap{
"toYaml": ToYaml,
}
for name, f := range extraFuncMap {
funcMap[name] = f
}
return funcMap
}
Loading