Skip to content

Commit

Permalink
Allow users to inject a postrenderer (#219)
Browse files Browse the repository at this point in the history
* 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.
  • Loading branch information
ludydoo committed Aug 3, 2023
1 parent 9038198 commit caa0f13
Show file tree
Hide file tree
Showing 3 changed files with 127 additions and 3 deletions.
21 changes: 18 additions & 3 deletions pkg/client/actionclient.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,13 +99,21 @@ func AppendInstallFailureUninstallOptions(opts ...UninstallOption) ActionClientG
return nil
}
}

func AppendUpgradeFailureRollbackOptions(opts ...RollbackOption) ActionClientGetterOption {
return func(getter *actionClientGetter) error {
getter.upgradeFailureRollbackOpts = append(getter.upgradeFailureRollbackOpts, opts...)
return nil
}
}

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 {
Expand All @@ -126,6 +134,8 @@ type actionClientGetter struct {

installFailureUninstallOpts []UninstallOption
upgradeFailureRollbackOpts []RollbackOption

postRendererProviders []PostRendererProvider
}

var _ ActionClientGetter = &actionClientGetter{}
Expand All @@ -139,16 +149,21 @@ 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,

// For the install and upgrade options, we put the post renderer first in the list
// 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,
Expand Down
105 changes: 105 additions & 0 deletions pkg/client/actionclient_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,10 @@ limitations under the License.
package client

import (
"bytes"
"context"
"errors"
"io"
"strconv"
"time"

Expand All @@ -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"
Expand All @@ -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"
Expand Down Expand Up @@ -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)
})

Expand Down Expand Up @@ -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())
Expand Down Expand Up @@ -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
}
}
4 changes: 4 additions & 0 deletions pkg/client/postrenderer.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down

0 comments on commit caa0f13

Please sign in to comment.