Skip to content

Commit

Permalink
Fix panic when trying to replace placeholders
Browse files Browse the repository at this point in the history
There were cases of panic when the SyncConfig has items that contain arrays
of primitive types.
  • Loading branch information
ccremer committed Nov 12, 2020
1 parent 84ee437 commit e7806bf
Show file tree
Hide file tree
Showing 5 changed files with 123 additions and 30 deletions.
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ $(TESTBIN_DIR):
integration_test: export ENVTEST_K8S_VERSION = 1.19.0
integration_test: generate fmt vet manifests $(TESTBIN_DIR)
test -f ${ENVTEST_ASSETS_DIR}/setup-envtest.sh || curl -sSLo ${ENVTEST_ASSETS_DIR}/setup-envtest.sh https://raw.githubusercontent.com/kubernetes-sigs/controller-runtime/master/hack/setup-envtest.sh
source ${ENVTEST_ASSETS_DIR}/setup-envtest.sh; fetch_envtest_tools $(ENVTEST_ASSETS_DIR); setup_envtest_env $(ENVTEST_ASSETS_DIR); go test ./... -coverprofile cover.out -ginkgo.v
source ${ENVTEST_ASSETS_DIR}/setup-envtest.sh; fetch_envtest_tools $(ENVTEST_ASSETS_DIR); setup_envtest_env $(ENVTEST_ASSETS_DIR); go test -v ./... -coverprofile cover.out

.PHONY: dist
dist: generate fmt vet
Expand Down
23 changes: 9 additions & 14 deletions controllers/syncconfig_controller_test.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,3 @@
/*
Licensed under the Apache License, Version 2.0 (the "License");
http://www.apache.org/licenses/LICENSE-2.0
*/

package controllers

import (
Expand Down Expand Up @@ -114,10 +109,10 @@ var _ = Describe("SyncConfig controller", func() {
Expect(result).ToNot(BeNil())

By("verify that resource exist")
syncResult := &v1.ConfigMap{}
key := types.NamespacedName{Namespace: ns, Name: cm.Name}
Expect(k8sClient.Get(context.Background(), key, syncResult)).ToNot(HaveOccurred())
Expect(syncResult.Data["PROJECT_NAME"]).To(BeEquivalentTo(ns))
cmResult := &v1.ConfigMap{}
cmKey := types.NamespacedName{Namespace: ns, Name: cm.Name}
Expect(k8sClient.Get(context.Background(), cmKey, cmResult)).ToNot(HaveOccurred())
Expect(cmResult.Data["PROJECT_NAME"]).To(BeEquivalentTo(ns))

newSC := &SyncConfig{ObjectMeta: toObjectMeta(sc.Name, sc.Namespace)}
err = k8sClient.Get(context.Background(), toObjectKey(newSC), newSC)
Expand Down Expand Up @@ -233,12 +228,12 @@ var _ = Describe("SyncConfig controller", func() {
})
})

func toUnstructured(configMap *v1.ConfigMap) unstructured.Unstructured {
obj := unstructured.Unstructured{}
m, err := runtime.DefaultUnstructuredConverter.ToUnstructured(configMap)
func toUnstructured(obj interface{}) unstructured.Unstructured {
converted := unstructured.Unstructured{}
m, err := runtime.DefaultUnstructuredConverter.ToUnstructured(obj)
Expect(err).ToNot(HaveOccurred())
obj.SetUnstructuredContent(m)
return obj
converted.SetUnstructuredContent(m)
return converted
}

func toObjectKey(config *SyncConfig) types.NamespacedName {
Expand Down
68 changes: 53 additions & 15 deletions controllers/utils.go
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
package controllers

import (
"errors"
"fmt"
"github.com/vshn/espejo/api/v1alpha1"
"k8s.io/api/core/v1"
v12 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
ctrl "sigs.k8s.io/controller-runtime"
"strings"
)

var log = ctrl.Log.WithName("utils")

func getLoggingKeysAndValues(unstructuredObject *unstructured.Unstructured) []interface{} {
return []interface{}{
"Object.Kind", unstructuredObject.GetKind(),
Expand All @@ -22,28 +27,61 @@ func getLoggingKeysAndValuesForSyncConfig(syncconfig *v1alpha1.SyncConfig) []int
}
}

// replaceProjectName recursively replaces all string occurrences that contain the ${PROJECT_NAME} as placeholder.
// Only replaces the values of objects, does not alter the keys.
func replaceProjectName(replacement string, m map[string]interface{}) {
for k, v := range m {
if v == nil {
continue
}
switch v.(type) {
case string:
s := m[k].(string)
m[k] = strings.ReplaceAll(s, "${PROJECT_NAME}", replacement)
case int64:
case int32:
case int:
case bool:
continue
case []interface{}:
for _, elem := range v.([]interface{}) {
replaceProjectName(replacement, elem.(map[string]interface{}))
}
case interface{}:
replaceProjectName(replacement, m[k].(map[string]interface{}))
m[k] = transcendStructure(replacement, v)
}
}

func transcendStructure(replacement string, v interface{}) interface{} {
if v == nil {
return nil
}
switch v.(type) {
case string:
return replacePlaceholders(replacement, v.(string))
case []string:
for i, a := range v.([]string) {
v.([]string)[i] = replacePlaceholders(replacement, a)
}
return v
case int64:
case []int64:
case int32:
case []int32:
case int:
case []int:
case bool:
case []bool:
return v
case map[string]interface{}:
for k, m := range v.(map[string]interface{}) {
v.(map[string]interface{})[k] = transcendStructure(replacement, m)
}
return v
case []map[string]interface{}:
for k, m := range v.([]map[string]interface{}) {
v.([]map[string]interface{})[k] = transcendStructure(replacement, m).(map[string]interface{})
}
return v
case []interface{}:
for i, a := range v.([]interface{}) {
v.([]interface{})[i] = transcendStructure(replacement, a)
}
return v
default:
log.Error(errors.New(fmt.Sprintf("unrecognized type: %s is %T", v, v)), "Cannot replace placeholders in structure")
}
return v
}

func replacePlaceholders(replacement, s string) string {
return strings.ReplaceAll(s, "${PROJECT_NAME}", replacement)
}

func namespaceFromString(namespace string) v1.Namespace {
Expand Down
59 changes: 59 additions & 0 deletions controllers/utils_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package controllers

import (
"github.com/stretchr/testify/assert"
"testing"
)

func Test_Replacement(t *testing.T) {
replacement := "my-string"
placeholder := "${PROJECT_NAME}"

var m map[string]interface{}
m = map[string]interface{}{
"object-with-nested-objects": map[string]interface{}{
"object": map[string]interface{}{
"string-field": placeholder,
},
},
"slice-with-strings": []string{placeholder},
"slice-with-other-types": []int{0, 1},
"slice-with-nested-objects": []map[string]interface{}{
{
"object": map[string]interface{}{
"string-field": placeholder,
},
},
{
"string-field": placeholder,
},
},
"slice-with-nested-slices": []interface{}{
[]string{placeholder},
map[string]interface{}{
"string-field": placeholder,
"bool-field": true,
},
[]interface{}{
[]string{placeholder},
},
[]map[string]interface{}{
{
"string-field": placeholder,
},
},
},
}

replaceProjectName(replacement, m)

assert.Equal(t, replacement, m["object-with-nested-objects"].(map[string]interface{})["object"].(map[string]interface{})["string-field"])
assert.Equal(t, replacement, m["slice-with-strings"].([]string)[0])
assert.Equal(t, replacement, m["slice-with-nested-objects"].([]map[string]interface{})[0]["object"].(map[string]interface{})["string-field"])
assert.Equal(t, replacement, m["slice-with-nested-objects"].([]map[string]interface{})[1]["string-field"])
assert.Equal(t, replacement, m["slice-with-nested-slices"].([]interface{})[0].([]string)[0])
assert.Equal(t, replacement, m["slice-with-nested-slices"].([]interface{})[1].(map[string]interface{})["string-field"])
assert.Equal(t, replacement, m["slice-with-nested-slices"].([]interface{})[2].([]interface{})[0].([]string)[0])
assert.Equal(t, replacement, m["slice-with-nested-slices"].([]interface{})[3].([]map[string]interface{})[0]["string-field"])

}
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ require (
github.com/onsi/ginkgo v1.14.2
github.com/onsi/gomega v1.10.3
github.com/spf13/pflag v1.0.5
github.com/stretchr/testify v1.4.0
go.uber.org/zap v1.16.0
k8s.io/api v0.18.6
k8s.io/apimachinery v0.18.6
Expand Down

0 comments on commit e7806bf

Please sign in to comment.