From caa0f13c4bfc00fbfa76481d52c77b033b08182b Mon Sep 17 00:00:00 2001 From: Ludovic Cleroux Date: Thu, 3 Aug 2023 16:04:04 +0200 Subject: [PATCH] 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. --- pkg/client/actionclient.go | 21 ++++++- pkg/client/actionclient_test.go | 105 ++++++++++++++++++++++++++++++++ pkg/client/postrenderer.go | 4 ++ 3 files changed, 127 insertions(+), 3 deletions(-) diff --git a/pkg/client/actionclient.go b/pkg/client/actionclient.go index ce8429aa..f1421b58 100644 --- a/pkg/client/actionclient.go +++ b/pkg/client/actionclient.go @@ -99,6 +99,7 @@ func AppendInstallFailureUninstallOptions(opts ...UninstallOption) ActionClientG return nil } } + func AppendUpgradeFailureRollbackOptions(opts ...RollbackOption) ActionClientGetterOption { return func(getter *actionClientGetter) error { getter.upgradeFailureRollbackOpts = append(getter.upgradeFailureRollbackOpts, opts...) @@ -106,6 +107,13 @@ func AppendUpgradeFailureRollbackOptions(opts ...RollbackOption) ActionClientGet } } +func AppendPostRenderers(postRendererFns ...PostRendererProvider) ActionClientGetterOption { + return func(getter *actionClientGetter) error { + getter.postRendererProviders = append(getter.postRendererProviders, postRendererFns...) + return nil + } +} + func NewActionClientGetter(acg ActionConfigGetter, opts ...ActionClientGetterOption) (ActionClientGetter, error) { actionClientGetter := &actionClientGetter{acg: acg} for _, opt := range opts { @@ -126,6 +134,8 @@ type actionClientGetter struct { installFailureUninstallOpts []UninstallOption upgradeFailureRollbackOpts []RollbackOption + + postRendererProviders []PostRendererProvider } var _ ActionClientGetter = &actionClientGetter{} @@ -139,7 +149,12 @@ func (hcg *actionClientGetter) ActionClientFor(obj client.Object) (ActionInterfa if err != nil { return nil, err } - postRenderer := DefaultPostRendererFunc(rm, actionConfig.KubeClient, obj) + var cpr = chainedPostRenderer{} + for _, provider := range hcg.postRendererProviders { + cpr = append(cpr, provider(rm, actionConfig.KubeClient, obj)) + } + cpr = append(cpr, DefaultPostRendererFunc(rm, actionConfig.KubeClient, obj)) + return &actionClient{ conf: actionConfig, @@ -147,8 +162,8 @@ func (hcg *actionClientGetter) ActionClientFor(obj client.Object) (ActionInterfa // on purpose because we want user-provided defaults to be able to override the // post-renderer that we automatically configure for the client. defaultGetOpts: hcg.defaultGetOpts, - defaultInstallOpts: append([]InstallOption{WithInstallPostRenderer(postRenderer)}, hcg.defaultInstallOpts...), - defaultUpgradeOpts: append([]UpgradeOption{WithUpgradePostRenderer(postRenderer)}, hcg.defaultUpgradeOpts...), + defaultInstallOpts: append([]InstallOption{WithInstallPostRenderer(cpr)}, hcg.defaultInstallOpts...), + defaultUpgradeOpts: append([]UpgradeOption{WithUpgradePostRenderer(cpr)}, hcg.defaultUpgradeOpts...), defaultUninstallOpts: hcg.defaultUninstallOpts, installFailureUninstallOpts: hcg.installFailureUninstallOpts, diff --git a/pkg/client/actionclient_test.go b/pkg/client/actionclient_test.go index a33ee5d6..8e590087 100644 --- a/pkg/client/actionclient_test.go +++ b/pkg/client/actionclient_test.go @@ -17,8 +17,10 @@ limitations under the License. package client import ( + "bytes" "context" "errors" + "io" "strconv" "time" @@ -27,6 +29,8 @@ import ( . "github.com/onsi/gomega" "helm.sh/helm/v3/pkg/action" "helm.sh/helm/v3/pkg/chartutil" + "helm.sh/helm/v3/pkg/kube" + "helm.sh/helm/v3/pkg/postrender" "helm.sh/helm/v3/pkg/release" "helm.sh/helm/v3/pkg/releaseutil" "helm.sh/helm/v3/pkg/storage/driver" @@ -36,6 +40,7 @@ import ( "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" apitypes "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/util/rand" @@ -78,12 +83,15 @@ var _ = Describe("ActionClient", func() { var ( actionConfigGetter ActionConfigGetter + cli kube.Interface obj client.Object ) BeforeEach(func() { var err error actionConfigGetter, err = NewActionConfigGetter(cfg, rm, logr.Discard()) Expect(err).ShouldNot(HaveOccurred()) + cli = kube.New(newRESTClientGetter(cfg, rm, "")) + Expect(err).ShouldNot(HaveOccurred()) obj = testutil.BuildTestCR(gvk) }) @@ -236,6 +244,40 @@ var _ = Describe("ActionClient", func() { }) Expect(err).To(MatchError(ContainSubstring(expectErr.Error()))) + // Uninstall the chart to cleanup for other tests. + _, err = ac.Uninstall(obj.GetName()) + Expect(err).To(BeNil()) + }) + It("should get clients with postrenderers", func() { + + acg, err := NewActionClientGetter(actionConfigGetter, AppendPostRenderers(newMockPostRenderer("foo", "bar"))) + Expect(err).To(BeNil()) + Expect(acg).NotTo(BeNil()) + + ac, err := acg.ActionClientFor(obj) + Expect(err).To(BeNil()) + + _, err = ac.Install(obj.GetName(), obj.GetNamespace(), &chrt, chartutil.Values{}) + Expect(err).To(BeNil()) + + rel, err := ac.Get(obj.GetName()) + Expect(err).To(BeNil()) + + rl, err := cli.Build(bytes.NewBufferString(rel.Manifest), false) + Expect(err).To(BeNil()) + + Expect(rl).NotTo(BeEmpty()) + err = rl.Visit(func(info *resource.Info, err error) error { + Expect(err).To(BeNil()) + Expect(info.Object).NotTo(BeNil()) + objMeta, err := meta.Accessor(info.Object) + Expect(err).To(BeNil()) + Expect(objMeta.GetAnnotations()).To(HaveKey("foo")) + Expect(objMeta.GetAnnotations()["foo"]).To(Equal("bar")) + return nil + }) + Expect(err).To(BeNil()) + // Uninstall the chart to cleanup for other tests. _, err = ac.Uninstall(obj.GetName()) Expect(err).To(BeNil()) @@ -807,3 +849,66 @@ func newTestDeployment(containers []v1.Container) *appsv1.Deployment { }, } } + +type mockPostRenderer struct { + k8sCli kube.Interface + key string + value string +} + +var _ postrender.PostRenderer = &mockPostRenderer{} + +func newMockPostRenderer(key, value string) PostRendererProvider { + return func(rm meta.RESTMapper, kubeClient kube.Interface, obj client.Object) postrender.PostRenderer { + return &mockPostRenderer{ + k8sCli: kubeClient, + key: key, + value: value, + } + } +} + +func (m *mockPostRenderer) Run(renderedManifests *bytes.Buffer) (modifiedManifests *bytes.Buffer, err error) { + b, err := io.ReadAll(renderedManifests) + if err != nil { + return nil, err + } + rl, err := m.k8sCli.Build(bytes.NewBuffer(b), false) + if err != nil { + return nil, err + } + out := bytes.Buffer{} + if err := rl.Visit(m.visit(&out)); err != nil { + return nil, err + } + return &out, nil +} + +func (m *mockPostRenderer) visit(out *bytes.Buffer) func(r *resource.Info, err error) error { + return func(r *resource.Info, rErr error) error { + if rErr != nil { + return rErr + } + objMap, err := runtime.DefaultUnstructuredConverter.ToUnstructured(r.Object) + if err != nil { + return err + } + u := &unstructured.Unstructured{Object: objMap} + + annotations := u.GetAnnotations() + if annotations == nil { + annotations = map[string]string{} + } + annotations[m.key] = m.value + u.SetAnnotations(annotations) + + outData, err := yaml.Marshal(u.Object) + if err != nil { + return err + } + if _, err := out.WriteString("---\n" + string(outData)); err != nil { + return err + } + return nil + } +} diff --git a/pkg/client/postrenderer.go b/pkg/client/postrenderer.go index 81d57b28..85983b7d 100644 --- a/pkg/client/postrenderer.go +++ b/pkg/client/postrenderer.go @@ -20,6 +20,10 @@ import ( "github.com/operator-framework/helm-operator-plugins/pkg/manifestutil" ) +// PostRendererProvider is a function that returns a post-renderer for a given object. +// obj represents the custom resource that is being reconciled. +type PostRendererProvider func(rm meta.RESTMapper, kubeClient kube.Interface, obj client.Object) postrender.PostRenderer + // WithInstallPostRenderer sets the post-renderer to use for the install. // It overrides any post-renderer that may already be configured or set // as a default.