Skip to content

Commit

Permalink
fix(11164): Advanced templating using patchTemplate
Browse files Browse the repository at this point in the history
Signed-off-by: gmuselli <[email protected]>
  • Loading branch information
speedfl committed Nov 26, 2023
1 parent 1600c03 commit fcbe22a
Show file tree
Hide file tree
Showing 17 changed files with 1,108 additions and 671 deletions.
29 changes: 29 additions & 0 deletions applicationset/controllers/applicationset_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -524,6 +524,7 @@ func (r *ApplicationSetReconciler) generateApplications(logCtx *log.Entry, appli

for _, p := range a.Params {
app, err := r.Renderer.RenderTemplateParams(tmplApplication, applicationSetInfo.Spec.SyncPolicy, p, applicationSetInfo.Spec.GoTemplate, applicationSetInfo.Spec.GoTemplateOptions)

if err != nil {
logCtx.WithError(err).WithField("params", a.Params).WithField("generator", requestedGenerator).
Error("error generating application from params")
Expand All @@ -534,6 +535,24 @@ func (r *ApplicationSetReconciler) generateApplications(logCtx *log.Entry, appli
}
continue
}

if applicationSetInfo.Spec.TemplatePatch != nil {
patchedApplication, err := r.applyTemplatePatch(app, applicationSetInfo, p)

if err != nil {
log.WithError(err).WithField("params", a.Params).WithField("generator", requestedGenerator).
Error("error generating application from params")

if firstError == nil {
firstError = err
applicationSetReason = argov1alpha1.ApplicationSetReasonRenderTemplateParamsError
}
continue
}

app = patchedApplication
}

res = append(res, *app)
}
}
Expand All @@ -545,6 +564,16 @@ func (r *ApplicationSetReconciler) generateApplications(logCtx *log.Entry, appli
return res, applicationSetReason, firstError
}

func (r *ApplicationSetReconciler) applyTemplatePatch(app *argov1alpha1.Application, applicationSetInfo argov1alpha1.ApplicationSet, params map[string]interface{}) (*argov1alpha1.Application, error) {
replacedTemplate, err := r.Renderer.Replace(*applicationSetInfo.Spec.TemplatePatch, params, applicationSetInfo.Spec.GoTemplate, applicationSetInfo.Spec.GoTemplateOptions)

if err != nil {
return nil, err
}

return utils.ApplyPatchTemplate(app, replacedTemplate)
}

func ignoreNotAllowedNamespaces(namespaces []string) predicate.Predicate {
return predicate.Funcs{
CreateFunc: func(e event.CreateEvent) bool {
Expand Down
12 changes: 12 additions & 0 deletions applicationset/controllers/applicationset_controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,12 @@ func (g *generatorMock) GenerateParams(appSetGenerator *v1alpha1.ApplicationSetG
return args.Get(0).([]map[string]interface{}), args.Error(1)
}

func (g *generatorMock) Replace(tmpl string, replaceMap map[string]interface{}, useGoTemplate bool, goTemplateOptions []string) (string, error) {
args := g.Called(tmpl, replaceMap, useGoTemplate, goTemplateOptions)

return args.Get(0).(string), args.Error(1)
}

type rendererMock struct {
mock.Mock
}
Expand All @@ -107,6 +113,12 @@ func (r *rendererMock) RenderTemplateParams(tmpl *v1alpha1.Application, syncPoli

}

func (r *rendererMock) Replace(tmpl string, replaceMap map[string]interface{}, useGoTemplate bool, goTemplateOptions []string) (string, error) {
args := r.Called(tmpl, replaceMap, useGoTemplate, goTemplateOptions)

return args.Get(0).(string), args.Error(1)
}

func TestExtractApplications(t *testing.T) {
scheme := runtime.NewScheme()
err := v1alpha1.AddToScheme(scheme)
Expand Down
42 changes: 42 additions & 0 deletions applicationset/utils/templatePatch.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package utils

import (
"encoding/json"
"fmt"

"k8s.io/apimachinery/pkg/util/strategicpatch"

appv1 "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1"
)

func ApplyPatchTemplate(app *appv1.Application, templatePatch string) (*appv1.Application, error) {

appString, err := json.Marshal(app)
if err != nil {
return nil, fmt.Errorf("error while marhsalling Application %w", err)
}

convertedTemplatePatch, err := ConvertYAMLToJSON(templatePatch)

if err != nil {
return nil, fmt.Errorf("error while converting template to json %q: %w", convertedTemplatePatch, err)
}

if err := json.Unmarshal([]byte(convertedTemplatePatch), &appv1.Application{}); err != nil {
return nil, fmt.Errorf("invalid templatePatch %q: %w", convertedTemplatePatch, err)
}

data, err := strategicpatch.StrategicMergePatch([]byte(appString), []byte(convertedTemplatePatch), appv1.Application{})

if err != nil {
return nil, fmt.Errorf("error while applying templatePatch template to json %q: %w", convertedTemplatePatch, err)
}

finalApp := appv1.Application{}
err = json.Unmarshal(data, &finalApp)
if err != nil {
return nil, fmt.Errorf("error while unmarhsalling patched application %w", err)
}

return &finalApp, err
}
97 changes: 97 additions & 0 deletions applicationset/utils/templatePatch_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
package utils

import (
"testing"

appv1 "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

func validate(t *testing.T, patch string) {
app := &appv1.Application{
TypeMeta: metav1.TypeMeta{
Kind: "Application",
APIVersion: "argoproj.io/v1alpha1",
},
ObjectMeta: metav1.ObjectMeta{
Name: "my-cluster-guestbook",
Namespace: "namespace",
Finalizers: []string{"resources-finalizer.argocd.argoproj.io"},
},
Spec: appv1.ApplicationSpec{
Project: "default",
Source: &appv1.ApplicationSource{
RepoURL: "https://github.com/argoproj/argocd-example-apps.git",
TargetRevision: "HEAD",
Path: "guestbook",
},
Destination: appv1.ApplicationDestination{
Server: "https://kubernetes.default.svc",
Namespace: "guestbook",
},
},
}

result, err := ApplyPatchTemplate(app, patch)
require.NoError(t, err)
require.Contains(t, result.ObjectMeta.Annotations, "annotation-some-key")
assert.Equal(t, result.ObjectMeta.Annotations["annotation-some-key"], "annotation-some-value")

assert.Equal(t, result.Spec.SyncPolicy.Automated.Prune, true)
require.Contains(t, result.Spec.Source.Helm.ValueFiles, "values.test.yaml")
require.Contains(t, result.Spec.Source.Helm.ValueFiles, "values.big.yaml")
}

func TestWithJson(t *testing.T) {

validate(t, `{
"metadata": {
"annotations": {
"annotation-some-key": "annotation-some-value"
}
},
"spec": {
"source": {
"helm": {
"valueFiles": [
"values.test.yaml",
"values.big.yaml"
]
}
},
"syncPolicy": {
"automated": {
"prune": true
}
}
}
}`)

}

func TestWithYaml(t *testing.T) {

validate(t, `
metadata:
annotations:
annotation-some-key: annotation-some-value
spec:
source:
helm:
valueFiles:
- values.test.yaml
- values.big.yaml
syncPolicy:
automated:
prune: true`)
}

func TestError(t *testing.T) {
app := &appv1.Application{}

result, err := ApplyPatchTemplate(app, "hello world")
require.Error(t, err)
require.Nil(t, result)
}
1 change: 1 addition & 0 deletions applicationset/utils/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ func init() {

type Renderer interface {
RenderTemplateParams(tmpl *argoappsv1.Application, syncPolicy *argoappsv1.ApplicationSetSyncPolicy, params map[string]interface{}, useGoTemplate bool, goTemplateOptions []string) (*argoappsv1.Application, error)
Replace(tmpl string, replaceMap map[string]interface{}, useGoTemplate bool, goTemplateOptions []string) (string, error)
}

type Render struct {
Expand Down
3 changes: 3 additions & 0 deletions assets/swagger.json
Original file line number Diff line number Diff line change
Expand Up @@ -6143,6 +6143,9 @@
},
"template": {
"$ref": "#/definitions/v1alpha1ApplicationSetTemplate"
},
"templatePatch": {
"type": "string"
}
}
},
Expand Down
57 changes: 57 additions & 0 deletions docs/operator-manual/applicationset/Template.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,3 +108,60 @@ spec:
(*The full example can be found [here](https://github.com/argoproj/argo-cd/tree/master/applicationset/examples/template-override).*)
In this example, the ApplicationSet controller will generate an `Application` resource using the `path` generated by the List generator, rather than the `path` value defined in `.spec.template`.

## Patch Template

Templating is only available on string type. However some uses cases may require to apply templating on other types.

Example:

- Set the automated sync policy
- Switch prune boolean to true
- Add multiple helm value files

ArgoCD has a `templatePatch` feature to allow advanced templating. It supports both json and yaml


```yaml
apiVersion: argoproj.io/v1alpha1
kind: ApplicationSet
metadata:
name: guestbook
spec:
goTemplate: true
generators:
- list:
elements:
- cluster: engineering-dev
url: https://kubernetes.default.svc
autoSync: true
prune: true
valueFiles:
- values.large.yaml
- values.debug.yaml
templatePatch: |
spec:
source:
helm:
valueFiles:
{{- range $valueFile := .valueFiles }}
- {{ $valueFile }}
{{- end }}
{{- if .autoSync }}
syncPolicy:
automated:
prune: {{ .prune }}
{{- end }}
template:
metadata:
name: '{{.cluster}}-deployment'
spec:
project: "default"
source:
repoURL: https://github.com/infra-team/cluster-deployments.git
targetRevision: HEAD
path: guestbook/{{ .cluster }}
destination:
server: '{{.url}}'
namespace: guestbook
```
2 changes: 2 additions & 0 deletions manifests/core-install.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -20123,6 +20123,8 @@ spec:
- metadata
- spec
type: object
templatePatch:
type: string
required:
- generators
- template
Expand Down
2 changes: 2 additions & 0 deletions manifests/crds/applicationset-crd.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15183,6 +15183,8 @@ spec:
- metadata
- spec
type: object
templatePatch:
type: string
required:
- generators
- template
Expand Down
2 changes: 2 additions & 0 deletions manifests/ha/install.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -20123,6 +20123,8 @@ spec:
- metadata
- spec
type: object
templatePatch:
type: string
required:
- generators
- template
Expand Down
2 changes: 2 additions & 0 deletions manifests/install.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -20123,6 +20123,8 @@ spec:
- metadata
- spec
type: object
templatePatch:
type: string
required:
- generators
- template
Expand Down
1 change: 1 addition & 0 deletions pkg/apis/application/v1alpha1/applicationset_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ type ApplicationSetSpec struct {
// ApplyNestedSelectors enables selectors defined within the generators of two level-nested matrix or merge generators
ApplyNestedSelectors bool `json:"applyNestedSelectors,omitempty" protobuf:"bytes,8,name=applyNestedSelectors"`
IgnoreApplicationDifferences ApplicationSetIgnoreDifferences `json:"ignoreApplicationDifferences,omitempty" protobuf:"bytes,9,name=ignoreApplicationDifferences"`
TemplatePatch *string `json:"templatePatch,omitempty" protobuf:"bytes,10,name=templatePatch"`
}

type ApplicationPreservedFields struct {
Expand Down
Loading

0 comments on commit fcbe22a

Please sign in to comment.