Skip to content

Commit a423fef

Browse files
authored
Add a WithValueTranslator option to Reconciller. (#114)
A Translator is a way to produces helm values based on the fetched custom resource itself (unlike `Mapper` which can only see `Values`). This way the code which converts the custom resource to Helm values can first convert an `Unstructured` into a regular struct, and then rely on Go type safety rather than work with a tree of maps from `string` to `interface{}`. Thanks to having access to a `Context`, the code can also safely access the network, for example in order to retrieve other resources from the k8s cluster, when they are referenced by the custom resource.
1 parent 6968556 commit a423fef

File tree

5 files changed

+205
-88
lines changed

5 files changed

+205
-88
lines changed

pkg/reconciler/internal/values/values.go

Lines changed: 22 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -17,21 +17,37 @@ limitations under the License.
1717
package values
1818

1919
import (
20+
"context"
2021
"fmt"
21-
"os"
22-
2322
"helm.sh/helm/v3/pkg/chartutil"
2423
"helm.sh/helm/v3/pkg/strvals"
2524
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
25+
"os"
2626

2727
"github.com/operator-framework/helm-operator-plugins/pkg/values"
2828
)
2929

30-
type Values struct {
31-
m map[string]interface{}
30+
var DefaultMapper = values.MapperFunc(func(v chartutil.Values) chartutil.Values { return v })
31+
32+
var DefaultTranslator = values.TranslatorFunc(func(ctx context.Context, u *unstructured.Unstructured) (chartutil.Values, error) {
33+
return getSpecMap(u)
34+
})
35+
36+
func ApplyOverrides(overrideValues map[string]string, obj *unstructured.Unstructured) error {
37+
specMap, err := getSpecMap(obj)
38+
if err != nil {
39+
return err
40+
}
41+
for inK, inV := range overrideValues {
42+
val := fmt.Sprintf("%s=%s", inK, os.ExpandEnv(inV))
43+
if err := strvals.ParseInto(val, specMap); err != nil {
44+
return err
45+
}
46+
}
47+
return nil
3248
}
3349

34-
func FromUnstructured(obj *unstructured.Unstructured) (*Values, error) {
50+
func getSpecMap(obj *unstructured.Unstructured) (map[string]interface{}, error) {
3551
if obj == nil || obj.Object == nil {
3652
return nil, fmt.Errorf("nil object")
3753
}
@@ -43,28 +59,5 @@ func FromUnstructured(obj *unstructured.Unstructured) (*Values, error) {
4359
if !ok {
4460
return nil, fmt.Errorf("spec must be a map")
4561
}
46-
return New(specMap), nil
47-
}
48-
49-
func New(m map[string]interface{}) *Values {
50-
return &Values{m: m}
51-
}
52-
53-
func (v *Values) Map() map[string]interface{} {
54-
if v == nil {
55-
return nil
56-
}
57-
return v.m
62+
return specMap, nil
5863
}
59-
60-
func (v *Values) ApplyOverrides(in map[string]string) error {
61-
for inK, inV := range in {
62-
val := fmt.Sprintf("%s=%s", inK, os.ExpandEnv(inV))
63-
if err := strvals.ParseInto(val, v.m); err != nil {
64-
return err
65-
}
66-
}
67-
return nil
68-
}
69-
70-
var DefaultMapper = values.MapperFunc(func(v chartutil.Values) chartutil.Values { return v })

pkg/reconciler/internal/values/values_test.go

Lines changed: 43 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ limitations under the License.
1717
package values_test
1818

1919
import (
20+
"context"
2021
. "github.com/onsi/ginkgo"
2122
. "github.com/onsi/gomega"
2223
"helm.sh/helm/v3/pkg/chartutil"
@@ -25,73 +26,50 @@ import (
2526
. "github.com/operator-framework/helm-operator-plugins/pkg/reconciler/internal/values"
2627
)
2728

28-
var _ = Describe("Values", func() {
29-
var _ = Describe("FromUnstructured", func() {
30-
It("should error with nil object", func() {
31-
u := &unstructured.Unstructured{}
32-
v, err := FromUnstructured(u)
33-
Expect(v).To(BeNil())
34-
Expect(err).NotTo(BeNil())
35-
})
29+
var _ = Describe("ApplyOverrides", func() {
30+
var u *unstructured.Unstructured
3631

37-
It("should error with missing spec", func() {
38-
u := &unstructured.Unstructured{Object: map[string]interface{}{}}
39-
v, err := FromUnstructured(u)
40-
Expect(v).To(BeNil())
41-
Expect(err).NotTo(BeNil())
32+
When("Unstructured object is invalid", func() {
33+
It("should error with nil unstructured", func() {
34+
u = nil
35+
Expect(ApplyOverrides(nil, u)).NotTo(BeNil())
4236
})
4337

44-
It("should error with non-map spec", func() {
45-
u := &unstructured.Unstructured{Object: map[string]interface{}{"spec": 0}}
46-
v, err := FromUnstructured(u)
47-
Expect(v).To(BeNil())
48-
Expect(err).NotTo(BeNil())
38+
It("should error with nil object", func() {
39+
u = &unstructured.Unstructured{}
40+
Expect(ApplyOverrides(nil, u)).NotTo(BeNil())
4941
})
5042

51-
It("should succeed with valid spec", func() {
52-
values := New(map[string]interface{}{"foo": "bar"})
53-
u := &unstructured.Unstructured{Object: map[string]interface{}{"spec": values.Map()}}
54-
Expect(FromUnstructured(u)).To(Equal(values))
43+
It("should error with missing spec", func() {
44+
u = &unstructured.Unstructured{Object: map[string]interface{}{}}
45+
Expect(ApplyOverrides(nil, u)).NotTo(BeNil())
5546
})
56-
})
5747

58-
var _ = Describe("New", func() {
59-
It("should return new values", func() {
60-
m := map[string]interface{}{"foo": "bar"}
61-
v := New(m)
62-
Expect(v.Map()).To(Equal(m))
48+
It("should error with non-map spec", func() {
49+
u = &unstructured.Unstructured{Object: map[string]interface{}{"spec": 0}}
50+
Expect(ApplyOverrides(nil, u)).NotTo(BeNil())
6351
})
6452
})
6553

66-
var _ = Describe("Map", func() {
67-
It("should return nil with nil values", func() {
68-
var v *Values
69-
Expect(v.Map()).To(BeNil())
70-
})
54+
When("Unstructured object is valid", func() {
7155

72-
It("should return values as a map", func() {
73-
m := map[string]interface{}{"foo": "bar"}
74-
v := New(m)
75-
Expect(v.Map()).To(Equal(m))
56+
BeforeEach(func() {
57+
u = &unstructured.Unstructured{Object: map[string]interface{}{"spec": map[string]interface{}{}}}
7658
})
77-
})
7859

79-
var _ = Describe("ApplyOverrides", func() {
8060
It("should succeed with empty values", func() {
81-
v := New(map[string]interface{}{})
82-
Expect(v.ApplyOverrides(map[string]string{"foo": "bar"})).To(Succeed())
83-
Expect(v.Map()).To(Equal(map[string]interface{}{"foo": "bar"}))
61+
Expect(ApplyOverrides(map[string]string{"foo": "bar"}, u)).To(Succeed())
62+
Expect(u.Object).To(Equal(map[string]interface{}{"spec": map[string]interface{}{"foo": "bar"}}))
8463
})
8564

86-
It("should succeed with empty values", func() {
87-
v := New(map[string]interface{}{"foo": "bar"})
88-
Expect(v.ApplyOverrides(map[string]string{"foo": "baz"})).To(Succeed())
89-
Expect(v.Map()).To(Equal(map[string]interface{}{"foo": "baz"}))
65+
It("should succeed with non-empty values", func() {
66+
u.Object["spec"].(map[string]interface{})["foo"] = "bar"
67+
Expect(ApplyOverrides(map[string]string{"foo": "baz"}, u)).To(Succeed())
68+
Expect(u.Object).To(Equal(map[string]interface{}{"spec": map[string]interface{}{"foo": "baz"}}))
9069
})
9170

9271
It("should fail with invalid overrides", func() {
93-
v := New(map[string]interface{}{"foo": "bar"})
94-
Expect(v.ApplyOverrides(map[string]string{"foo[": "test"})).ToNot(BeNil())
72+
Expect(ApplyOverrides(map[string]string{"foo[": "test"}, u)).ToNot(BeNil())
9573
})
9674
})
9775
})
@@ -103,3 +81,20 @@ var _ = Describe("DefaultMapper", func() {
10381
Expect(out).To(Equal(in))
10482
})
10583
})
84+
85+
var _ = Describe("DefaultTranslator", func() {
86+
var m map[string]interface{}
87+
88+
It("returns empty spec untouched", func() {
89+
m = map[string]interface{}{}
90+
})
91+
92+
It("returns filled spec untouched", func() {
93+
m = map[string]interface{}{"something": 0}
94+
})
95+
96+
AfterEach(func() {
97+
u := &unstructured.Unstructured{Object: map[string]interface{}{"spec": m}}
98+
Expect(DefaultTranslator.Translate(context.Background(), u)).To(Equal(chartutil.Values(m)))
99+
})
100+
})

pkg/reconciler/reconciler.go

Lines changed: 42 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,8 @@ const uninstallFinalizer = "uninstall-helm-release"
6363
type Reconciler struct {
6464
client client.Client
6565
actionClientGetter helmclient.ActionClientGetter
66-
valueMapper values.Mapper
66+
valueTranslator values.Translator
67+
valueMapper values.Mapper // nolint:staticcheck
6768
eventRecorder record.EventRecorder
6869
preHooks []hook.PreHook
6970
postHooks []hook.PostHook
@@ -234,8 +235,8 @@ func WithOverrideValues(overrides map[string]string) Option {
234235
// Validate that overrides can be parsed and applied
235236
// so that we fail fast during operator setup rather
236237
// than during the first reconciliation.
237-
m := internalvalues.New(map[string]interface{}{})
238-
if err := m.ApplyOverrides(overrides); err != nil {
238+
obj := &unstructured.Unstructured{Object: map[string]interface{}{"spec": map[string]interface{}{}}}
239+
if err := internalvalues.ApplyOverrides(overrides, obj); err != nil {
239240
return err
240241
}
241242

@@ -378,8 +379,36 @@ func WithPostHook(h hook.PostHook) Option {
378379
}
379380
}
380381

382+
// WithValueTranslator is an Option that configures a function that translates a
383+
// custom resource to the values passed to Helm.
384+
// Use this if you need to customize the logic that translates your custom resource to Helm values.
385+
// If you wish to, you can convert the Unstructured that is passed to your Translator to your own
386+
// Custom Resource struct like this:
387+
//
388+
// import "k8s.io/apimachinery/pkg/runtime"
389+
// foo := your.Foo{}
390+
// if err = runtime.DefaultUnstructuredConverter.FromUnstructured(u.Object, &foo); err != nil {
391+
// return nil, err
392+
// }
393+
// // work with the type-safe foo
394+
//
395+
// Alternatively, your translator can also work similarly to a Mapper, by accessing the spec with:
396+
//
397+
// u.Object["spec"].(map[string]interface{})
398+
func WithValueTranslator(t values.Translator) Option {
399+
return func(r *Reconciler) error {
400+
r.valueTranslator = t
401+
return nil
402+
}
403+
}
404+
381405
// WithValueMapper is an Option that configures a function that maps values
382-
// from a custom resource spec to the values passed to Helm
406+
// from a custom resource spec to the values passed to Helm.
407+
// Use this if you want to apply a transformation on the values obtained from your custom resource, before
408+
// they are passed to Helm.
409+
//
410+
// Deprecated: Use WithValueTranslator instead.
411+
// WithValueMapper will be removed in a future release.
383412
func WithValueMapper(m values.Mapper) Option {
384413
return func(r *Reconciler) error {
385414
r.valueMapper = m
@@ -483,7 +512,7 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (res ctrl.
483512
return ctrl.Result{}, err
484513
}
485514

486-
vals, err := r.getValues(obj)
515+
vals, err := r.getValues(ctx, obj)
487516
if err != nil {
488517
u.UpdateStatus(
489518
updater.EnsureCondition(conditions.Irreconcilable(corev1.ConditionTrue, conditions.ReasonErrorGettingValues, err)),
@@ -546,15 +575,15 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (res ctrl.
546575
return ctrl.Result{RequeueAfter: r.reconcilePeriod}, nil
547576
}
548577

549-
func (r *Reconciler) getValues(obj *unstructured.Unstructured) (chartutil.Values, error) {
550-
crVals, err := internalvalues.FromUnstructured(obj)
551-
if err != nil {
578+
func (r *Reconciler) getValues(ctx context.Context, obj *unstructured.Unstructured) (chartutil.Values, error) {
579+
if err := internalvalues.ApplyOverrides(r.overrideValues, obj); err != nil {
552580
return chartutil.Values{}, err
553581
}
554-
if err := crVals.ApplyOverrides(r.overrideValues); err != nil {
582+
vals, err := r.valueTranslator.Translate(ctx, obj)
583+
if err != nil {
555584
return chartutil.Values{}, err
556585
}
557-
vals := r.valueMapper.Map(crVals.Map())
586+
vals = r.valueMapper.Map(vals)
558587
vals, err = chartutil.CoalesceValues(r.chrt, vals)
559588
if err != nil {
560589
return chartutil.Values{}, err
@@ -773,6 +802,9 @@ func (r *Reconciler) addDefaults(mgr ctrl.Manager, controllerName string) {
773802
if r.eventRecorder == nil {
774803
r.eventRecorder = mgr.GetEventRecorderFor(controllerName)
775804
}
805+
if r.valueTranslator == nil {
806+
r.valueTranslator = internalvalues.DefaultTranslator
807+
}
776808
if r.valueMapper == nil {
777809
r.valueMapper = internalvalues.DefaultMapper
778810
}

0 commit comments

Comments
 (0)