Skip to content

Commit 57dfe5d

Browse files
authored
Replace WithValueTranslator implementation with upstream one (#23)
* Revert Translator commits. Revert "Pass context to Translate(). (#8)" This reverts commit c3df552. Revert "Add a WithValueTranslator option to Reconciller. (#6)" This reverts commit 88508a2. * Add a WithValueTranslator option to Reconciller (cherry-picked from upstream PR). 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 1d724c0 commit 57dfe5d

File tree

5 files changed

+173
-96
lines changed

5 files changed

+173
-96
lines changed

pkg/reconciler/internal/values/values.go

Lines changed: 21 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -19,20 +19,35 @@ package values
1919
import (
2020
"context"
2121
"fmt"
22-
"os"
23-
2422
"helm.sh/helm/v3/pkg/chartutil"
2523
"helm.sh/helm/v3/pkg/strvals"
2624
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
25+
"os"
2726

2827
"github.com/joelanford/helm-operator/pkg/values"
2928
)
3029

31-
type Values struct {
32-
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
3348
}
3449

35-
func FromUnstructured(obj *unstructured.Unstructured) (*Values, error) {
50+
func getSpecMap(obj *unstructured.Unstructured) (map[string]interface{}, error) {
3651
if obj == nil || obj.Object == nil {
3752
return nil, fmt.Errorf("nil object")
3853
}
@@ -44,36 +59,5 @@ func FromUnstructured(obj *unstructured.Unstructured) (*Values, error) {
4459
if !ok {
4560
return nil, fmt.Errorf("spec must be a map")
4661
}
47-
return New(specMap), nil
48-
}
49-
50-
func New(m map[string]interface{}) *Values {
51-
return &Values{m: m}
52-
}
53-
54-
func (v *Values) Map() map[string]interface{} {
55-
if v == nil {
56-
return nil
57-
}
58-
return v.m
62+
return specMap, nil
5963
}
60-
61-
func (v *Values) ApplyOverrides(in map[string]string) error {
62-
for inK, inV := range in {
63-
val := fmt.Sprintf("%s=%s", inK, os.ExpandEnv(inV))
64-
if err := strvals.ParseInto(val, v.m); err != nil {
65-
return err
66-
}
67-
}
68-
return nil
69-
}
70-
71-
var DefaultMapper = values.MapperFunc(func(v chartutil.Values) chartutil.Values { return v })
72-
73-
var DefaultTranslator = values.TranslatorFunc(func(ctx context.Context, u *unstructured.Unstructured) (chartutil.Values, error) {
74-
internalValues, err := FromUnstructured(u)
75-
if err != nil {
76-
return chartutil.Values{}, err
77-
}
78-
return internalValues.Map(), err
79-
})

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/joelanford/helm-operator/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: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ type Reconciler struct {
6464
client client.Client
6565
actionClientGetter helmclient.ActionClientGetter
6666
valueTranslator values.Translator
67-
valueMapper values.Mapper
67+
valueMapper values.Mapper // nolint:staticcheck
6868
eventRecorder record.EventRecorder
6969
preHooks []hook.PreHook
7070
postHooks []hook.PostHook
@@ -253,8 +253,8 @@ func WithOverrideValues(overrides map[string]string) Option {
253253
// Validate that overrides can be parsed and applied
254254
// so that we fail fast during operator setup rather
255255
// than during the first reconciliation.
256-
m := internalvalues.New(map[string]interface{}{})
257-
if err := m.ApplyOverrides(overrides); err != nil {
256+
obj := &unstructured.Unstructured{Object: map[string]interface{}{"spec": map[string]interface{}{}}}
257+
if err := internalvalues.ApplyOverrides(overrides, obj); err != nil {
258258
return err
259259
}
260260

@@ -453,6 +453,19 @@ func WithPostExtension(e extensions.ReconcileExtension) Option {
453453
// WithValueTranslator is an Option that configures a function that translates a
454454
// custom resource to the values passed to Helm.
455455
// Use this if you need to customize the logic that translates your custom resource to Helm values.
456+
// If you wish to, you can convert the Unstructured that is passed to your Translator to your own
457+
// Custom Resource struct like this:
458+
//
459+
// import "k8s.io/apimachinery/pkg/runtime"
460+
// foo := your.Foo{}
461+
// if err = runtime.DefaultUnstructuredConverter.FromUnstructured(u.Object, &foo); err != nil {
462+
// return nil, err
463+
// }
464+
// // work with the type-safe foo
465+
//
466+
// Alternatively, your translator can also work similarly to a Mapper, by accessing the spec with:
467+
//
468+
// u.Object["spec"].(map[string]interface{})
456469
func WithValueTranslator(t values.Translator) Option {
457470
return func(r *Reconciler) error {
458471
r.valueTranslator = t
@@ -464,6 +477,9 @@ func WithValueTranslator(t values.Translator) Option {
464477
// from a custom resource spec to the values passed to Helm.
465478
// Use this if you want to apply a transformation on the values obtained from your custom resource, before
466479
// they are passed to Helm.
480+
//
481+
// Deprecated: Use WithValueTranslator instead.
482+
// WithValueMapper will be removed in a future release.
467483
func WithValueMapper(m values.Mapper) Option {
468484
return func(r *Reconciler) error {
469485
r.valueMapper = m
@@ -665,15 +681,14 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (res ctrl.
665681
}
666682

667683
func (r *Reconciler) getValues(ctx context.Context, obj *unstructured.Unstructured) (chartutil.Values, error) {
668-
vals, err := r.valueTranslator.Translate(ctx, obj)
669-
if err != nil {
684+
if err := internalvalues.ApplyOverrides(r.overrideValues, obj); err != nil {
670685
return chartutil.Values{}, err
671686
}
672-
crVals := internalvalues.New(vals)
673-
if err := crVals.ApplyOverrides(r.overrideValues); err != nil {
687+
vals, err := r.valueTranslator.Translate(ctx, obj)
688+
if err != nil {
674689
return chartutil.Values{}, err
675690
}
676-
vals = r.valueMapper.Map(crVals.Map())
691+
vals = r.valueMapper.Map(vals)
677692
vals, err = chartutil.CoalesceValues(r.chrt, vals)
678693
if err != nil {
679694
return chartutil.Values{}, err

pkg/reconciler/reconciler_test.go

Lines changed: 76 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -382,6 +382,16 @@ var _ = Describe("Reconciler", func() {
382382
Expect(r.valueMapper.Map(chartutil.Values{})).To(Equal(chartutil.Values{"mapped": true}))
383383
})
384384
})
385+
var _ = Describe("WithValueTranslator", func() {
386+
It("should set the reconciler value translator", func() {
387+
translator := values.TranslatorFunc(func(ctx context.Context, u *unstructured.Unstructured) (chartutil.Values, error) {
388+
return chartutil.Values{"translated": true}, nil
389+
})
390+
Expect(WithValueTranslator(translator)(r)).To(Succeed())
391+
Expect(r.valueTranslator).NotTo(BeNil())
392+
Expect(r.valueTranslator.Translate(context.Background(), &unstructured.Unstructured{})).To(Equal(chartutil.Values{"translated": true}))
393+
})
394+
})
385395
})
386396

387397
var _ = Describe("Reconcile", func() {
@@ -560,6 +570,7 @@ var _ = Describe("Reconciler", func() {
560570
By("reconciling unsuccessfully", func() {
561571
res, err := r.Reconcile(ctx, req)
562572
Expect(res).To(Equal(reconcile.Result{}))
573+
Expect(err).ToNot(BeNil())
563574
Expect(err.Error()).To(ContainSubstring("error parsing index"))
564575
})
565576

@@ -805,6 +816,7 @@ var _ = Describe("Reconciler", func() {
805816
By("reconciling unsuccessfully", func() {
806817
res, err := r.Reconcile(ctx, req)
807818
Expect(res).To(Equal(reconcile.Result{}))
819+
Expect(err).ToNot(BeNil())
808820
Expect(err.Error()).To(ContainSubstring("error parsing index"))
809821
})
810822

@@ -834,6 +846,45 @@ var _ = Describe("Reconciler", func() {
834846
})
835847
})
836848
})
849+
When("value translator fails", func() {
850+
BeforeEach(func() {
851+
r.valueTranslator = values.TranslatorFunc(func(ctx context.Context, u *unstructured.Unstructured) (chartutil.Values, error) {
852+
return nil, errors.New("translation failure")
853+
})
854+
})
855+
It("returns an error", func() {
856+
By("reconciling unsuccessfully", func() {
857+
res, err := r.Reconcile(ctx, req)
858+
Expect(res).To(Equal(reconcile.Result{}))
859+
Expect(err.Error()).To(ContainSubstring("translation failure"))
860+
})
861+
862+
By("getting the CR", func() {
863+
Expect(mgr.GetAPIReader().Get(ctx, objKey, obj)).To(Succeed())
864+
})
865+
866+
By("verifying the CR status", func() {
867+
objStat := &objStatus{}
868+
Expect(runtime.DefaultUnstructuredConverter.FromUnstructured(obj.Object, objStat)).To(Succeed())
869+
Expect(objStat.Status.Conditions.IsTrueFor(conditions.TypeInitialized)).To(BeTrue())
870+
Expect(objStat.Status.Conditions.IsTrueFor(conditions.TypeIrreconcilable)).To(BeTrue())
871+
Expect(objStat.Status.Conditions.IsTrueFor(conditions.TypeDeployed)).To(BeTrue())
872+
Expect(objStat.Status.Conditions.IsUnknownFor(conditions.TypeReleaseFailed)).To(BeTrue())
873+
874+
c := objStat.Status.Conditions.GetCondition(conditions.TypeIrreconcilable)
875+
Expect(c).NotTo(BeNil())
876+
Expect(c.Reason).To(Equal(conditions.ReasonErrorGettingValues))
877+
Expect(c.Message).To(ContainSubstring("translation failure"))
878+
879+
Expect(objStat.Status.DeployedRelease.Name).To(Equal(currentRelease.Name))
880+
Expect(objStat.Status.DeployedRelease.Manifest).To(Equal(currentRelease.Manifest))
881+
})
882+
883+
By("verifying the uninstall finalizer is not present on the CR", func() {
884+
Expect(controllerutil.ContainsFinalizer(obj, uninstallFinalizer)).To(BeTrue())
885+
})
886+
})
887+
})
837888
When("requested CR release is not deployed", func() {
838889
var actionConf *action.Configuration
839890
BeforeEach(func() {
@@ -1231,14 +1282,38 @@ func verifyRelease(ctx context.Context, cl client.Reader, ns string, rel *releas
12311282
}
12321283
})
12331284

1285+
var objs []client.Object
1286+
12341287
By("verifying the release resources exist", func() {
1235-
objs := manifestToObjects(rel.Manifest)
1288+
objs = manifestToObjects(rel.Manifest)
12361289
for _, obj := range objs {
12371290
key := client.ObjectKeyFromObject(obj)
12381291
err := cl.Get(ctx, key, obj)
12391292
Expect(err).To(BeNil())
12401293
}
12411294
})
1295+
1296+
By("verifying that deployment image was overridden", func() {
1297+
for _, obj := range objs {
1298+
if obj.GetName() == "test-test-chart" && obj.GetObjectKind().GroupVersionKind().Kind == "Deployment" {
1299+
expectDeploymentImagePrefix(obj, "custom-nginx:")
1300+
return
1301+
}
1302+
}
1303+
Fail("expected deployment not found")
1304+
})
1305+
}
1306+
1307+
func expectDeploymentImagePrefix(obj client.Object, prefix string) {
1308+
u := obj.(*unstructured.Unstructured)
1309+
containers, ok, err := unstructured.NestedSlice(u.Object, "spec", "template", "spec", "containers")
1310+
Expect(ok).To(BeTrue())
1311+
Expect(err).To(BeNil())
1312+
container := containers[0].(map[string]interface{})
1313+
val, ok, err := unstructured.NestedString(container, "image")
1314+
Expect(ok).To(BeTrue())
1315+
Expect(err).To(BeNil())
1316+
Expect(val).To(HavePrefix(prefix))
12421317
}
12431318

12441319
func verifyNoRelease(ctx context.Context, cl client.Client, ns string, name string, rel *release.Release) {

0 commit comments

Comments
 (0)