Skip to content

Commit

Permalink
fix(appset): Fix helm valuesObject with ApplicationSet (argoproj#14912)…
Browse files Browse the repository at this point in the history
… (argoproj#14920)

Signed-off-by: Geoffrey Muselli <[email protected]>
  • Loading branch information
speedfl authored and tesla59 committed Dec 16, 2023
1 parent 0133be8 commit a144bc3
Show file tree
Hide file tree
Showing 3 changed files with 222 additions and 6 deletions.
40 changes: 34 additions & 6 deletions applicationset/utils/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import (

"github.com/Masterminds/sprig/v3"
"github.com/valyala/fasttemplate"
"sigs.k8s.io/yaml"

log "github.com/sirupsen/logrus"

Expand Down Expand Up @@ -51,6 +52,22 @@ func copyUnexported(copy, original reflect.Value) {
copyValueIntoUnexported(copy, unexported)
}

func IsJSONStr(str string) bool {
str = strings.TrimSpace(str)
return len(str) > 0 && str[0] == '{'
}

func ConvertYAMLToJSON(str string) (string, error) {
if !IsJSONStr(str) {
jsonStr, err := yaml.YAMLToJSON([]byte(str))
if err != nil {
return str, err
}
return string(jsonStr), nil
}
return str, nil
}

// This function is in charge of searching all String fields of the object recursively and apply templating
// thanks to https://gist.github.com/randallmlough/1fd78ec8a1034916ca52281e3b886dc7
func (r *Render) deeplyReplace(copy, original reflect.Value, replaceMap map[string]interface{}, useGoTemplate bool, goTemplateOptions []string) error {
Expand Down Expand Up @@ -86,11 +103,18 @@ func (r *Render) deeplyReplace(copy, original reflect.Value, replaceMap map[stri
originalValue := original.Elem()
// Create a new object. Now new gives us a pointer, but we want the value it
// points to, so we have to call Elem() to unwrap it
copyValue := reflect.New(originalValue.Type()).Elem()
if err := r.deeplyReplace(copyValue, originalValue, replaceMap, useGoTemplate, goTemplateOptions); err != nil {
return err

if originalValue.IsValid() {
reflectType := originalValue.Type()

reflectValue := reflect.New(reflectType)

copyValue := reflectValue.Elem()
if err := r.deeplyReplace(copyValue, originalValue, replaceMap, useGoTemplate, goTemplateOptions); err != nil {
return err
}
copy.Set(copyValue)
}
copy.Set(copyValue)

// If it is a struct we translate each field
case reflect.Struct:
Expand All @@ -99,10 +123,14 @@ func (r *Render) deeplyReplace(copy, original reflect.Value, replaceMap map[stri
// specific case time
if currentType == "time.Time" {
copy.Field(i).Set(original.Field(i))
} else if currentType == "Raw.k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" {
} else if currentType == "Raw.k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" || currentType == "Raw.k8s.io/apimachinery/pkg/runtime" {
var unmarshaled interface{}
originalBytes := original.Field(i).Bytes()
err := json.Unmarshal(originalBytes, &unmarshaled)
convertedToJson, err := ConvertYAMLToJSON(string(originalBytes))
if err != nil {
return fmt.Errorf("error while converting template to json %q: %w", convertedToJson, err)
}
err = json.Unmarshal([]byte(convertedToJson), &unmarshaled)
if err != nil {
return fmt.Errorf("failed to unmarshal JSON field: %w", err)
}
Expand Down
108 changes: 108 additions & 0 deletions applicationset/utils/utils_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package utils

import (
"crypto/x509"
"encoding/json"
"os"
"path"
"testing"
Expand Down Expand Up @@ -198,6 +199,113 @@ func TestRenderTemplateParams(t *testing.T) {

}

func TestRenderHelmValuesObjectJson(t *testing.T) {

params := map[string]interface{}{
"test": "Hello world",
}

application := &argoappsv1.Application{
ObjectMeta: metav1.ObjectMeta{
Annotations: map[string]string{"annotation-key": "annotation-value", "annotation-key2": "annotation-value2"},
Labels: map[string]string{"label-key": "label-value", "label-key2": "label-value2"},
CreationTimestamp: metav1.NewTime(time.Now()),
UID: types.UID("d546da12-06b7-4f9a-8ea2-3adb16a20e2b"),
Name: "application-one",
Namespace: "default",
},
Spec: argoappsv1.ApplicationSpec{
Source: &argoappsv1.ApplicationSource{
Path: "",
RepoURL: "",
TargetRevision: "",
Chart: "",
Helm: &argoappsv1.ApplicationSourceHelm{
ValuesObject: &runtime.RawExtension{
Raw: []byte(`{
"some": {
"string": "{{.test}}"
}
}`),
},
},
},
Destination: argoappsv1.ApplicationDestination{
Server: "",
Namespace: "",
Name: "",
},
Project: "",
},
}

// Render the cloned application, into a new application
render := Render{}
newApplication, err := render.RenderTemplateParams(application, nil, params, true, []string{})

assert.NoError(t, err)
assert.NotNil(t, newApplication)

var unmarshaled interface{}
err = json.Unmarshal(newApplication.Spec.Source.Helm.ValuesObject.Raw, &unmarshaled)

assert.NoError(t, err)
assert.Equal(t, unmarshaled.(map[string]interface{})["some"].(map[string]interface{})["string"], "Hello world")

}

func TestRenderHelmValuesObjectYaml(t *testing.T) {

params := map[string]interface{}{
"test": "Hello world",
}

application := &argoappsv1.Application{
ObjectMeta: metav1.ObjectMeta{
Annotations: map[string]string{"annotation-key": "annotation-value", "annotation-key2": "annotation-value2"},
Labels: map[string]string{"label-key": "label-value", "label-key2": "label-value2"},
CreationTimestamp: metav1.NewTime(time.Now()),
UID: types.UID("d546da12-06b7-4f9a-8ea2-3adb16a20e2b"),
Name: "application-one",
Namespace: "default",
},
Spec: argoappsv1.ApplicationSpec{
Source: &argoappsv1.ApplicationSource{
Path: "",
RepoURL: "",
TargetRevision: "",
Chart: "",
Helm: &argoappsv1.ApplicationSourceHelm{
ValuesObject: &runtime.RawExtension{
Raw: []byte(`some:
string: "{{.test}}"`),
},
},
},
Destination: argoappsv1.ApplicationDestination{
Server: "",
Namespace: "",
Name: "",
},
Project: "",
},
}

// Render the cloned application, into a new application
render := Render{}
newApplication, err := render.RenderTemplateParams(application, nil, params, true, []string{})

assert.NoError(t, err)
assert.NotNil(t, newApplication)

var unmarshaled interface{}
err = json.Unmarshal(newApplication.Spec.Source.Helm.ValuesObject.Raw, &unmarshaled)

assert.NoError(t, err)
assert.Equal(t, unmarshaled.(map[string]interface{})["some"].(map[string]interface{})["string"], "Hello world")

}

func TestRenderTemplateParamsGoTemplate(t *testing.T) {

// Believe it or not, this is actually less complex than the equivalent solution using reflection
Expand Down
80 changes: 80 additions & 0 deletions test/e2e/applicationset_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
corev1 "k8s.io/api/core/v1"
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"

"github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1"
argov1alpha1 "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1"
Expand Down Expand Up @@ -352,6 +353,85 @@ func TestSimpleListGeneratorGoTemplate(t *testing.T) {

}

func TestRenderHelmValuesObject(t *testing.T) {

expectedApp := argov1alpha1.Application{
TypeMeta: metav1.TypeMeta{
Kind: application.ApplicationKind,
APIVersion: "argoproj.io/v1alpha1",
},
ObjectMeta: metav1.ObjectMeta{
Name: "my-cluster-guestbook",
Namespace: fixture.TestNamespace(),
Finalizers: []string{"resources-finalizer.argocd.argoproj.io"},
Labels: map[string]string{
LabelKeyAppSetInstance: "test-values-object",
},
},
Spec: argov1alpha1.ApplicationSpec{
Project: "default",
Source: &argov1alpha1.ApplicationSource{
RepoURL: "https://github.com/argoproj/argocd-example-apps.git",
TargetRevision: "HEAD",
Path: "helm-guestbook",
Helm: &argov1alpha1.ApplicationSourceHelm{
ValuesObject: &runtime.RawExtension{
// This will always be converted as yaml
Raw: []byte(`{"some":{"string":"Hello world"}}`),
},
},
},
Destination: argov1alpha1.ApplicationDestination{
Server: "https://kubernetes.default.svc",
Namespace: "guestbook",
},
},
}

Given(t).
// Create a ListGenerator-based ApplicationSet
When().Create(v1alpha1.ApplicationSet{ObjectMeta: metav1.ObjectMeta{
Name: "test-values-object",
},
Spec: v1alpha1.ApplicationSetSpec{
GoTemplate: true,
Template: v1alpha1.ApplicationSetTemplate{
ApplicationSetTemplateMeta: v1alpha1.ApplicationSetTemplateMeta{Name: "{{.cluster}}-guestbook"},
Spec: argov1alpha1.ApplicationSpec{
Project: "default",
Source: &argov1alpha1.ApplicationSource{
RepoURL: "https://github.com/argoproj/argocd-example-apps.git",
TargetRevision: "HEAD",
Path: "helm-guestbook",
Helm: &argov1alpha1.ApplicationSourceHelm{
ValuesObject: &runtime.RawExtension{
Raw: []byte(`{"some":{"string":"{{.test}}"}}`),
},
},
},
Destination: argov1alpha1.ApplicationDestination{
Server: "{{.url}}",
Namespace: "guestbook",
},
},
},
Generators: []v1alpha1.ApplicationSetGenerator{
{
List: &v1alpha1.ListGenerator{
Elements: []apiextensionsv1.JSON{{
Raw: []byte(`{"cluster": "my-cluster","url": "https://kubernetes.default.svc", "test": "Hello world"}`),
}},
},
},
},
},
}).Then().Expect(ApplicationsExist([]argov1alpha1.Application{expectedApp})).
// Delete the ApplicationSet, and verify it deletes the Applications
When().
Delete().Then().Expect(ApplicationsDoNotExist([]argov1alpha1.Application{expectedApp}))

}

func TestSyncPolicyCreateUpdate(t *testing.T) {

expectedApp := argov1alpha1.Application{
Expand Down

0 comments on commit a144bc3

Please sign in to comment.