Skip to content

Commit caa0f13

Browse files
authored
Allow users to inject a postrenderer (#219)
* Allow user-provided post-renderers t would be useful for us to be able to inject a per-object PostRenderer to implement a feature such as istio overlays.
1 parent 9038198 commit caa0f13

File tree

3 files changed

+127
-3
lines changed

3 files changed

+127
-3
lines changed

pkg/client/actionclient.go

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -99,13 +99,21 @@ func AppendInstallFailureUninstallOptions(opts ...UninstallOption) ActionClientG
9999
return nil
100100
}
101101
}
102+
102103
func AppendUpgradeFailureRollbackOptions(opts ...RollbackOption) ActionClientGetterOption {
103104
return func(getter *actionClientGetter) error {
104105
getter.upgradeFailureRollbackOpts = append(getter.upgradeFailureRollbackOpts, opts...)
105106
return nil
106107
}
107108
}
108109

110+
func AppendPostRenderers(postRendererFns ...PostRendererProvider) ActionClientGetterOption {
111+
return func(getter *actionClientGetter) error {
112+
getter.postRendererProviders = append(getter.postRendererProviders, postRendererFns...)
113+
return nil
114+
}
115+
}
116+
109117
func NewActionClientGetter(acg ActionConfigGetter, opts ...ActionClientGetterOption) (ActionClientGetter, error) {
110118
actionClientGetter := &actionClientGetter{acg: acg}
111119
for _, opt := range opts {
@@ -126,6 +134,8 @@ type actionClientGetter struct {
126134

127135
installFailureUninstallOpts []UninstallOption
128136
upgradeFailureRollbackOpts []RollbackOption
137+
138+
postRendererProviders []PostRendererProvider
129139
}
130140

131141
var _ ActionClientGetter = &actionClientGetter{}
@@ -139,16 +149,21 @@ func (hcg *actionClientGetter) ActionClientFor(obj client.Object) (ActionInterfa
139149
if err != nil {
140150
return nil, err
141151
}
142-
postRenderer := DefaultPostRendererFunc(rm, actionConfig.KubeClient, obj)
152+
var cpr = chainedPostRenderer{}
153+
for _, provider := range hcg.postRendererProviders {
154+
cpr = append(cpr, provider(rm, actionConfig.KubeClient, obj))
155+
}
156+
cpr = append(cpr, DefaultPostRendererFunc(rm, actionConfig.KubeClient, obj))
157+
143158
return &actionClient{
144159
conf: actionConfig,
145160

146161
// For the install and upgrade options, we put the post renderer first in the list
147162
// on purpose because we want user-provided defaults to be able to override the
148163
// post-renderer that we automatically configure for the client.
149164
defaultGetOpts: hcg.defaultGetOpts,
150-
defaultInstallOpts: append([]InstallOption{WithInstallPostRenderer(postRenderer)}, hcg.defaultInstallOpts...),
151-
defaultUpgradeOpts: append([]UpgradeOption{WithUpgradePostRenderer(postRenderer)}, hcg.defaultUpgradeOpts...),
165+
defaultInstallOpts: append([]InstallOption{WithInstallPostRenderer(cpr)}, hcg.defaultInstallOpts...),
166+
defaultUpgradeOpts: append([]UpgradeOption{WithUpgradePostRenderer(cpr)}, hcg.defaultUpgradeOpts...),
152167
defaultUninstallOpts: hcg.defaultUninstallOpts,
153168

154169
installFailureUninstallOpts: hcg.installFailureUninstallOpts,

pkg/client/actionclient_test.go

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,10 @@ limitations under the License.
1717
package client
1818

1919
import (
20+
"bytes"
2021
"context"
2122
"errors"
23+
"io"
2224
"strconv"
2325
"time"
2426

@@ -27,6 +29,8 @@ import (
2729
. "github.com/onsi/gomega"
2830
"helm.sh/helm/v3/pkg/action"
2931
"helm.sh/helm/v3/pkg/chartutil"
32+
"helm.sh/helm/v3/pkg/kube"
33+
"helm.sh/helm/v3/pkg/postrender"
3034
"helm.sh/helm/v3/pkg/release"
3135
"helm.sh/helm/v3/pkg/releaseutil"
3236
"helm.sh/helm/v3/pkg/storage/driver"
@@ -36,6 +40,7 @@ import (
3640
"k8s.io/apimachinery/pkg/api/meta"
3741
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
3842
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
43+
"k8s.io/apimachinery/pkg/runtime"
3944
"k8s.io/apimachinery/pkg/runtime/schema"
4045
apitypes "k8s.io/apimachinery/pkg/types"
4146
"k8s.io/apimachinery/pkg/util/rand"
@@ -78,12 +83,15 @@ var _ = Describe("ActionClient", func() {
7883

7984
var (
8085
actionConfigGetter ActionConfigGetter
86+
cli kube.Interface
8187
obj client.Object
8288
)
8389
BeforeEach(func() {
8490
var err error
8591
actionConfigGetter, err = NewActionConfigGetter(cfg, rm, logr.Discard())
8692
Expect(err).ShouldNot(HaveOccurred())
93+
cli = kube.New(newRESTClientGetter(cfg, rm, ""))
94+
Expect(err).ShouldNot(HaveOccurred())
8795
obj = testutil.BuildTestCR(gvk)
8896
})
8997

@@ -236,6 +244,40 @@ var _ = Describe("ActionClient", func() {
236244
})
237245
Expect(err).To(MatchError(ContainSubstring(expectErr.Error())))
238246

247+
// Uninstall the chart to cleanup for other tests.
248+
_, err = ac.Uninstall(obj.GetName())
249+
Expect(err).To(BeNil())
250+
})
251+
It("should get clients with postrenderers", func() {
252+
253+
acg, err := NewActionClientGetter(actionConfigGetter, AppendPostRenderers(newMockPostRenderer("foo", "bar")))
254+
Expect(err).To(BeNil())
255+
Expect(acg).NotTo(BeNil())
256+
257+
ac, err := acg.ActionClientFor(obj)
258+
Expect(err).To(BeNil())
259+
260+
_, err = ac.Install(obj.GetName(), obj.GetNamespace(), &chrt, chartutil.Values{})
261+
Expect(err).To(BeNil())
262+
263+
rel, err := ac.Get(obj.GetName())
264+
Expect(err).To(BeNil())
265+
266+
rl, err := cli.Build(bytes.NewBufferString(rel.Manifest), false)
267+
Expect(err).To(BeNil())
268+
269+
Expect(rl).NotTo(BeEmpty())
270+
err = rl.Visit(func(info *resource.Info, err error) error {
271+
Expect(err).To(BeNil())
272+
Expect(info.Object).NotTo(BeNil())
273+
objMeta, err := meta.Accessor(info.Object)
274+
Expect(err).To(BeNil())
275+
Expect(objMeta.GetAnnotations()).To(HaveKey("foo"))
276+
Expect(objMeta.GetAnnotations()["foo"]).To(Equal("bar"))
277+
return nil
278+
})
279+
Expect(err).To(BeNil())
280+
239281
// Uninstall the chart to cleanup for other tests.
240282
_, err = ac.Uninstall(obj.GetName())
241283
Expect(err).To(BeNil())
@@ -807,3 +849,66 @@ func newTestDeployment(containers []v1.Container) *appsv1.Deployment {
807849
},
808850
}
809851
}
852+
853+
type mockPostRenderer struct {
854+
k8sCli kube.Interface
855+
key string
856+
value string
857+
}
858+
859+
var _ postrender.PostRenderer = &mockPostRenderer{}
860+
861+
func newMockPostRenderer(key, value string) PostRendererProvider {
862+
return func(rm meta.RESTMapper, kubeClient kube.Interface, obj client.Object) postrender.PostRenderer {
863+
return &mockPostRenderer{
864+
k8sCli: kubeClient,
865+
key: key,
866+
value: value,
867+
}
868+
}
869+
}
870+
871+
func (m *mockPostRenderer) Run(renderedManifests *bytes.Buffer) (modifiedManifests *bytes.Buffer, err error) {
872+
b, err := io.ReadAll(renderedManifests)
873+
if err != nil {
874+
return nil, err
875+
}
876+
rl, err := m.k8sCli.Build(bytes.NewBuffer(b), false)
877+
if err != nil {
878+
return nil, err
879+
}
880+
out := bytes.Buffer{}
881+
if err := rl.Visit(m.visit(&out)); err != nil {
882+
return nil, err
883+
}
884+
return &out, nil
885+
}
886+
887+
func (m *mockPostRenderer) visit(out *bytes.Buffer) func(r *resource.Info, err error) error {
888+
return func(r *resource.Info, rErr error) error {
889+
if rErr != nil {
890+
return rErr
891+
}
892+
objMap, err := runtime.DefaultUnstructuredConverter.ToUnstructured(r.Object)
893+
if err != nil {
894+
return err
895+
}
896+
u := &unstructured.Unstructured{Object: objMap}
897+
898+
annotations := u.GetAnnotations()
899+
if annotations == nil {
900+
annotations = map[string]string{}
901+
}
902+
annotations[m.key] = m.value
903+
u.SetAnnotations(annotations)
904+
905+
outData, err := yaml.Marshal(u.Object)
906+
if err != nil {
907+
return err
908+
}
909+
if _, err := out.WriteString("---\n" + string(outData)); err != nil {
910+
return err
911+
}
912+
return nil
913+
}
914+
}

pkg/client/postrenderer.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,10 @@ import (
2020
"github.com/operator-framework/helm-operator-plugins/pkg/manifestutil"
2121
)
2222

23+
// PostRendererProvider is a function that returns a post-renderer for a given object.
24+
// obj represents the custom resource that is being reconciled.
25+
type PostRendererProvider func(rm meta.RESTMapper, kubeClient kube.Interface, obj client.Object) postrender.PostRenderer
26+
2327
// WithInstallPostRenderer sets the post-renderer to use for the install.
2428
// It overrides any post-renderer that may already be configured or set
2529
// as a default.

0 commit comments

Comments
 (0)