Skip to content

Commit

Permalink
Add Kustomization patch support to helm3 post render hook, resolves c…
Browse files Browse the repository at this point in the history
  • Loading branch information
lukeweber committed Aug 7, 2020
1 parent 056b309 commit fdea24d
Show file tree
Hide file tree
Showing 12 changed files with 665 additions and 31 deletions.
7 changes: 4 additions & 3 deletions apis/v1alpha1/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,9 +66,10 @@ type ValuesSpec struct {

// ReleaseParameters are the configurable fields of a Release.
type ReleaseParameters struct {
Chart ChartSpec `json:"chart"`
Namespace string `json:"namespace"`
ValuesSpec `json:",inline"`
Chart ChartSpec `json:"chart"`
Namespace string `json:"namespace"`
PatchesFrom []ValueFromSource `json:"patchesFrom,omitempty"`
ValuesSpec `json:",inline"`
}

// ReleaseObservation are the observable fields of a Release.
Expand Down
7 changes: 7 additions & 0 deletions apis/v1alpha1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

36 changes: 36 additions & 0 deletions config/crd/helm.crossplane.io_releases.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,42 @@ spec:
type: object
namespace:
type: string
patchesFrom:
items:
description: ValueFromSource represents source of a value
properties:
configMapKeyRef:
description: DataKeySelector defines required spec to access a key of a configmap or secret
properties:
key:
type: string
name:
type: string
namespace:
type: string
optional:
type: boolean
required:
- name
- namespace
type: object
secretKeyRef:
description: DataKeySelector defines required spec to access a key of a configmap or secret
properties:
key:
type: string
name:
type: string
namespace:
type: string
optional:
type: boolean
required:
- name
- namespace
type: object
type: object
type: array
set:
items:
description: SetVal represents a "set" value override in a Release
Expand Down
5 changes: 3 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ require (
github.com/crossplane/crossplane-tools v0.0.0-20200412230150-efd0edd4565b
github.com/google/go-cmp v0.4.0
github.com/pkg/errors v0.9.1
golang.org/x/tools v0.0.0-20191018212557-ed542cd5b28a
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543
golang.org/x/tools v0.0.0-20191018212557-ed542cd5b28a // indirect
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 // indirect
gopkg.in/alecthomas/kingpin.v2 v2.2.6
helm.sh/helm/v3 v3.2.4
k8s.io/api v0.18.5
Expand All @@ -19,5 +19,6 @@ require (
rsc.io/letsencrypt v0.0.3 // indirect
sigs.k8s.io/controller-runtime v0.6.1
sigs.k8s.io/controller-tools v0.3.0
sigs.k8s.io/kustomize/api v0.5.1
sigs.k8s.io/yaml v1.2.0
)
140 changes: 140 additions & 0 deletions go.sum

Large diffs are not rendered by default.

22 changes: 18 additions & 4 deletions pkg/clients/helm/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import (
"helm.sh/helm/v3/pkg/cli"
"helm.sh/helm/v3/pkg/release"
"k8s.io/client-go/rest"
ktype "sigs.k8s.io/kustomize/api/types"
)

const (
Expand All @@ -46,8 +47,8 @@ const (
// Client is the interface to interact with Helm
type Client interface {
GetLastRelease(release string) (*release.Release, error)
Install(release string, chartDef ChartDefinition, vals map[string]interface{}) (*release.Release, error)
Upgrade(release string, chartDef ChartDefinition, vals map[string]interface{}) (*release.Release, error)
Install(release string, chartDef ChartDefinition, vals map[string]interface{}, patches []ktype.Patch) (*release.Release, error)
Upgrade(release string, chartDef ChartDefinition, vals map[string]interface{}, patches []ktype.Patch) (*release.Release, error)
Rollback(release string) error
Uninstall(release string) error
}
Expand Down Expand Up @@ -136,18 +137,24 @@ func (hc *client) GetLastRelease(release string) (*release.Release, error) {
return hc.getClient.Run(release)
}

func (hc *client) Install(release string, chartDef ChartDefinition, vals map[string]interface{}) (*release.Release, error) {
func (hc *client) Install(release string, chartDef ChartDefinition, vals map[string]interface{}, patches []ktype.Patch) (*release.Release, error) {
hc.installClient.ReleaseName = release

c, err := hc.pullAndLoadChart(chartDef.Repository, chartDef.Name, chartDef.Version, chartDef.RepoUser, chartDef.RepoPass)
if err != nil {
return nil, err
}

if len(patches) > 0 {
hc.installClient.PostRenderer = &KustomizationRender{
patches: patches,
}
}

return hc.installClient.Run(c, vals)
}

func (hc *client) Upgrade(release string, chartDef ChartDefinition, vals map[string]interface{}) (*release.Release, error) {
func (hc *client) Upgrade(release string, chartDef ChartDefinition, vals map[string]interface{}, patches []ktype.Patch) (*release.Release, error) {
// Reset values so that source of truth for desired state is always the CR itself
hc.upgradeClient.ResetValues = true
hc.upgradeClient.MaxHistory = releaseMaxHistory
Expand All @@ -156,6 +163,13 @@ func (hc *client) Upgrade(release string, chartDef ChartDefinition, vals map[str
if err != nil {
return nil, err
}

if len(patches) > 0 {
hc.installClient.PostRenderer = &KustomizationRender{
patches: patches,
}
}

return hc.upgradeClient.Run(release, c, vals)
}

Expand Down
78 changes: 78 additions & 0 deletions pkg/clients/helm/kustomize.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package helm

import (
"bytes"
"encoding/json"
"io/ioutil"
"os"
"path/filepath"

"sigs.k8s.io/kustomize/api/filesys"
"sigs.k8s.io/kustomize/api/konfig"
"sigs.k8s.io/kustomize/api/krusty"
"sigs.k8s.io/kustomize/api/types"
)

type KustomizationRender struct {
patches []types.Patch
}

const (
kustomizationFileName = "kustomization.yaml"
helmOutputFileName = "helm-output.yaml"
)

func (kr KustomizationRender) Run(renderedManifests *bytes.Buffer) (modifiedManifests *bytes.Buffer, err error) {
d, err := ioutil.TempDir("", "helm-post-render")
if err != nil {
return nil, err
}

k := types.Kustomization{
Resources: []string{helmOutputFileName},
Patches: kr.patches,
}

fsys := filesys.MakeFsOnDisk()
kdata, err := json.Marshal(k)
if err != nil {
return nil, err
}

err = fsys.WriteFile(filepath.Join(d, kustomizationFileName), kdata)
if err != nil {
return nil, err
}

defer func() {
err := os.RemoveAll(d)
if err != nil {
// todo: log err
}
}()

err = fsys.WriteFile(filepath.Join(d, helmOutputFileName), renderedManifests.Bytes())
if err != nil {
return nil, err
}

opts := &krusty.Options{
DoLegacyResourceSort: false,
LoadRestrictions: types.LoadRestrictionsRootOnly,
DoPrune: false,
PluginConfig: konfig.DisabledPluginConfig(),
}

kust := krusty.MakeKustomizer(fsys, opts)
m, err := kust.Run(d)
if err != nil {
return nil, err
}

yml, err := m.AsYaml()
if err != nil {
return nil, err
}

return bytes.NewBuffer(yml), nil
}
102 changes: 102 additions & 0 deletions pkg/clients/helm/kustomize_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
package helm

import (
"bytes"
"fmt"
"reflect"
"testing"

"github.com/crossplane/crossplane-runtime/pkg/test"
"github.com/google/go-cmp/cmp"
"github.com/pkg/errors"
"sigs.k8s.io/kustomize/api/resid"
"sigs.k8s.io/kustomize/api/types"
)

const testDeployment = `
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment
spec:
selector:
matchLabels:
app: nginx
env: dev
template:
metadata:
labels:
app: nginx
env: dev
spec:
containers:
- name: nginx
image: nginx:1.14.2
ports:
- containerPort: 80
`

func TestKustomize(t *testing.T) {
type want struct {
result string
err error
}

tests := []struct {
name string
base string
patches []types.Patch
want want
}{
{
name: "BasicPatch",
base: testDeployment,
patches: []types.Patch{
{
Target: &types.Selector{Gvk: resid.Gvk{Kind: "Deployment"}},
Patch: "- op: add\n path: /spec/template/spec/nodeSelector\n value:\n node.size: really-big\n aws.az: us-west-2a",
},
},
want: want{
result: "apiVersion: apps/v1\nkind: Deployment\nmetadata:\n name: nginx-deployment\nspec:\n selector:\n matchLabels:\n app: nginx\n env: dev\n template:\n metadata:\n labels:\n app: nginx\n env: dev\n spec:\n containers:\n - image: nginx:1.14.2\n name: nginx\n ports:\n - containerPort: 80\n nodeSelector:\n aws.az: us-west-2a\n node.size: really-big\n",
},
},
{
name: "InvalidPatch",
base: testDeployment,
patches: []types.Patch{
{
Target: &types.Selector{Gvk: resid.Gvk{Kind: "Deployment"}},
Patch: "- bad patch",
},
},
want: want{
result: "",
err: errors.WithStack(fmt.Errorf("trouble configuring builtin PatchTransformer with config: `\npatch: '- bad patch'\ntarget:\n kind: Deployment\n`: unable to parse SM or JSON patch from [- bad patch]")),
},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
k := KustomizationRender{
patches: tt.patches,
}

buf := bytes.NewBuffer([]byte(tt.base))
resBuffer, gotErr := k.Run(buf)
gotResult := ""
if gotErr == nil {
gotResult = resBuffer.String()
}

if diff := cmp.Diff(tt.want.err, gotErr, test.EquateErrors()); diff != "" {
t.Errorf("Reconcile() -want error %s, +got error %s:\n%s", reflect.ValueOf(tt.want.err).Type(), reflect.ValueOf(gotErr).Type(), diff)
}

if diff := cmp.Diff(tt.want.result, gotResult); diff != "" {
t.Errorf("Reconcile() -want, +got:\n%s", diff)
}
})
}
}
41 changes: 41 additions & 0 deletions pkg/controller/patch.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package controller

import (
"context"

"github.com/crossplane-contrib/provider-helm/apis/v1alpha1"
"github.com/pkg/errors"
"sigs.k8s.io/controller-runtime/pkg/client"
ktypes "sigs.k8s.io/kustomize/api/types"
"sigs.k8s.io/yaml"
)

const (
keyDefaultPatchFrom = "patch.yaml"
errFailedToUnmarshallPatch = "failed to unmarshal patch"
)

func getPatchesFromSpec(ctx context.Context, kube client.Client, vals []v1alpha1.ValueFromSource) ([]ktypes.Patch, error) {
var base []ktypes.Patch

for _, vf := range vals {
s, err := getDataValueFromSource(ctx, kube, vf, keyDefaultPatchFrom)
if err != nil {
return nil, errors.Wrap(err, errFailedToGetValueFromSource)
}

if s == "" {
continue
}

var p struct {
Patches []ktypes.Patch `json:"patches"`
}
if err = yaml.Unmarshal([]byte(s), &p); err != nil {
return nil, errors.Wrap(err, errFailedToUnmarshallPatch)
}
base = append(base, p.Patches...)
}

return base, nil
}
Loading

0 comments on commit fdea24d

Please sign in to comment.