Skip to content

Commit 896f2a4

Browse files
committed
feat: Experimental support for Helm 3.0.0-beta.3
Tested with `helm diff upgrade` only. Changes for other helm-diff sub-commands will be made in coming weeks. Changes: - Migrated from Glide to Go modules as helm-diff was unable to build with Glide due to bunch of missing transitive dependencies probably due to that some projects are not managed by Glide - Auto-detection of the plugin context(helm 2 or 3) - `helm diff upgrade` has been enhanced so that it turns into Helm 3 mode Pre-requisites: - You need to `helm plugin install` with `helm` of 3.0.0-x as the plugin installation directory has been changed from that of helm 2.x.
1 parent 99b8474 commit 896f2a4

File tree

7 files changed

+981
-5
lines changed

7 files changed

+981
-5
lines changed

cmd/helm.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,13 @@ func locateChartPath(name, version string, verify bool, keyring string) (string,
7979
return name, fmt.Errorf("path %q not found", name)
8080
}
8181

82-
crepo := filepath.Join(helmpath.Home(homePath()).Repository(), name)
82+
var crepo string
83+
if os.Getenv("HELM_REPOSITORY_CONFIG") != "" {
84+
crepo = os.Getenv("HELM_REPOSITORY_CONFIG")
85+
} else {
86+
crepo = filepath.Join(helmpath.Home(homePath()).Repository(), name)
87+
}
88+
8389
if _, err := os.Stat(crepo); err == nil {
8490
return filepath.Abs(crepo)
8591
}

cmd/helm3.go

Lines changed: 323 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,323 @@
1+
package cmd
2+
3+
import (
4+
"fmt"
5+
"github.com/databus23/helm-diff/diff"
6+
"github.com/databus23/helm-diff/manifest"
7+
"github.com/pkg/errors"
8+
"helm.sh/helm/pkg/kube"
9+
"log"
10+
"os"
11+
"strings"
12+
"sync"
13+
14+
"helm.sh/helm/pkg/action"
15+
"helm.sh/helm/pkg/chart"
16+
"helm.sh/helm/pkg/chart/loader"
17+
"helm.sh/helm/pkg/cli"
18+
"helm.sh/helm/pkg/cli/values"
19+
"helm.sh/helm/pkg/getter"
20+
helm3release "helm.sh/helm/pkg/release"
21+
"helm.sh/helm/pkg/storage"
22+
"helm.sh/helm/pkg/storage/driver"
23+
24+
"k8s.io/cli-runtime/pkg/genericclioptions"
25+
26+
// Import to initialize client auth plugins.
27+
_ "k8s.io/client-go/plugin/pkg/client/auth"
28+
)
29+
30+
var (
31+
config genericclioptions.RESTClientGetter
32+
configOnce sync.Once
33+
envSettings *cli.EnvSettings
34+
)
35+
36+
func init() {
37+
envSettings = cli.New()
38+
}
39+
40+
// Helm3Client is the client for interacting with Helm3 releases
41+
type Helm3Client struct {
42+
conf *action.Configuration
43+
settings *cli.EnvSettings
44+
}
45+
46+
func helm3Run(d *diffCmd, chartPath string) error {
47+
name := d.release
48+
chart := d.chart
49+
50+
helm3 := NewHelm3()
51+
52+
releaseResponse, err := helm3.Get(name, 0)
53+
54+
var newInstall bool
55+
if err != nil && strings.Contains(err.Error(), fmt.Sprintf("release: %q not found", d.release)) {
56+
if d.allowUnreleased {
57+
fmt.Printf("********************\n\n\tRelease was not present in Helm. Diff will show entire contents as new.\n\n********************\n")
58+
newInstall = true
59+
err = nil
60+
} else {
61+
fmt.Printf("********************\n\n\tRelease was not present in Helm. Include the `--allow-unreleased` to perform diff without exiting in error.\n\n********************\n")
62+
}
63+
}
64+
65+
if err != nil {
66+
return prettyError(fmt.Errorf("get: %v", err))
67+
}
68+
69+
var currentSpecs, newSpecs map[string]*manifest.MappingResult
70+
valOpts := &values.Options{
71+
ValueFiles: d.valueFiles,
72+
Values: d.values,
73+
StringValues: d.stringValues,
74+
}
75+
if newInstall {
76+
installResponse, err := helm3.Install(d.release, chart, valOpts)
77+
if err != nil {
78+
return prettyError(fmt.Errorf("install: %v", err))
79+
}
80+
81+
currentSpecs = make(map[string]*manifest.MappingResult)
82+
newSpecs = manifest.Parse(installResponse.Manifest, installResponse.Namespace)
83+
} else {
84+
upgradeResponse, err := helm3.Upgrade(d.release, chart, valOpts)
85+
if err != nil {
86+
return prettyError(fmt.Errorf("upgrade: %v", err))
87+
}
88+
89+
if d.noHooks {
90+
currentSpecs = manifest.Parse(releaseResponse.Manifest, releaseResponse.Namespace)
91+
newSpecs = manifest.Parse(upgradeResponse.Manifest, upgradeResponse.Namespace)
92+
} else {
93+
currentSpecs = ParseRelease(releaseResponse, d.includeTests)
94+
newSpecs = ParseRelease(upgradeResponse, d.includeTests)
95+
}
96+
}
97+
98+
seenAnyChanges := diff.Manifests(currentSpecs, newSpecs, d.suppressedKinds, d.outputContext, os.Stdout)
99+
100+
if d.detailedExitCode && seenAnyChanges {
101+
return Error{
102+
error: errors.New("identified at least one change, exiting with non-zero exit code (detailed-exitcode parameter enabled)"),
103+
Code: 2,
104+
}
105+
}
106+
107+
return nil
108+
}
109+
110+
// ParseRelease parses Helm v3 release to obtain MappingResults
111+
func ParseRelease(release *helm3release.Release, includeTests bool) map[string]*manifest.MappingResult {
112+
man := release.Manifest
113+
for _, hook := range release.Hooks {
114+
if !includeTests && isTestHook(hook.Events) {
115+
continue
116+
}
117+
118+
man += "\n---\n"
119+
man += fmt.Sprintf("# Source: %s\n", hook.Path)
120+
man += hook.Manifest
121+
}
122+
return manifest.Parse(man, release.Namespace)
123+
}
124+
125+
func isTestHook(hookEvents []helm3release.HookEvent) bool {
126+
for _, event := range hookEvents {
127+
if event == helm3release.HookTest {
128+
return true
129+
}
130+
}
131+
132+
return false
133+
}
134+
135+
// NewHelm3 returns Helm3 client for use within helm-diff
136+
func NewHelm3() *Helm3Client {
137+
conf := &action.Configuration{}
138+
initActionConfig(conf, false)
139+
return &Helm3Client{
140+
conf: conf,
141+
settings: envSettings,
142+
}
143+
}
144+
145+
// Get returns the named release
146+
func (helm3 *Helm3Client) Get(name string, version int) (*helm3release.Release, error) {
147+
if version <= 0 {
148+
return helm3.conf.Releases.Last(name)
149+
}
150+
151+
return helm3.conf.Releases.Get(name, version)
152+
}
153+
154+
// Upgrade returns the named release after an upgrade
155+
func (helm3 *Helm3Client) Upgrade(name, chart string, valueOpts *values.Options) (*helm3release.Release, error) {
156+
conf := helm3.conf
157+
158+
settings := helm3.settings
159+
160+
client := action.NewUpgrade(conf)
161+
client.DryRun = true
162+
163+
getters := getter.All(settings)
164+
vals, err := valueOpts.MergeValues(getters)
165+
if err != nil {
166+
return nil, fmt.Errorf("merge values: %v", err)
167+
}
168+
169+
chartRequested, err := helm3.loadChart(chart, client.ChartPathOptions.LocateChart, settings)
170+
if err != nil {
171+
return nil, fmt.Errorf("load chart: %v", err)
172+
}
173+
174+
r, err := client.Run(name, chartRequested, vals)
175+
if err != nil {
176+
return nil, fmt.Errorf("run: %v", err)
177+
}
178+
return r, nil
179+
}
180+
181+
// Install returns the simulated release after installing
182+
func (helm3 *Helm3Client) Install(name, chart string, valueOpts *values.Options) (*helm3release.Release, error) {
183+
conf := helm3.conf
184+
185+
args := []string{name, chart}
186+
187+
settings := helm3.settings
188+
189+
client := action.NewInstall(conf)
190+
client.DryRun = true
191+
192+
name, chart, err := client.NameAndChart(args)
193+
if err != nil {
194+
return nil, err
195+
}
196+
client.ReleaseName = name
197+
198+
getters := getter.All(settings)
199+
vals, err := valueOpts.MergeValues(getters)
200+
if err != nil {
201+
return nil, err
202+
}
203+
204+
chartRequested, err := helm3.loadChart(chart, client.ChartPathOptions.LocateChart, settings)
205+
if err != nil {
206+
return nil, err
207+
}
208+
209+
client.Namespace = helm3.getNamespace()
210+
return client.Run(chartRequested, vals)
211+
}
212+
213+
//func (helm3 *Helm3Client) kubeConfig() genericclioptions.RESTClientGetter {
214+
// if helm3.config == nil {
215+
// settings := helm3.settings
216+
// helm3.config = kube.GetConfig(settings.KubeConfig, settings.KubeContext, settings.Namespace)
217+
// }
218+
// return helm3.config
219+
//}
220+
//
221+
func (helm3 *Helm3Client) getNamespace() string {
222+
if helm3.settings.Namespace != "" {
223+
return helm3.settings.Namespace
224+
}
225+
226+
//if ns, _, err := cli.kubeConfig().ToRawKubeConfigLoader().Namespace(); err == nil {
227+
// return ns
228+
//}
229+
return "default"
230+
}
231+
232+
func (helm3 *Helm3Client) loadChart(chart string, locateChart func(name string, settings *cli.EnvSettings) (string, error), settings *cli.EnvSettings) (*chart.Chart, error) {
233+
chartPath, err := locateChart(chart, settings)
234+
if err != nil {
235+
return nil, fmt.Errorf("locate chart: %v", err)
236+
}
237+
238+
debug("CHART PATH: %s\n", chartPath)
239+
240+
// Check chart dependencies to make sure all are present in /charts
241+
chartRequested, err := loader.Load(chartPath)
242+
if err != nil {
243+
return nil, fmt.Errorf("load: %v", err)
244+
}
245+
246+
validInstallableChart, checkErr := isChartInstallable(chartRequested)
247+
if !validInstallableChart {
248+
return nil, fmt.Errorf("invalid chart: checkErr=%v, err=%v", checkErr, err)
249+
}
250+
251+
return chartRequested, nil
252+
}
253+
254+
// isChartInstallable validates if a chart can be installed
255+
//
256+
// Application chart type is only installable
257+
func isChartInstallable(ch *chart.Chart) (bool, error) {
258+
switch ch.Metadata.Type {
259+
case "", "application":
260+
return true, nil
261+
}
262+
return false, errors.Errorf("%s charts are not installable", ch.Metadata.Type)
263+
}
264+
265+
func debug(msg string, args ...interface{}) {
266+
fmt.Fprintf(os.Stderr, msg, args...)
267+
}
268+
269+
func initActionConfig(actionConfig *action.Configuration, allNamespaces bool) {
270+
kc := kube.New(kubeConfig())
271+
kc.Log = debug
272+
273+
clientset, err := kc.Factory.KubernetesClientSet()
274+
if err != nil {
275+
// TODO return error
276+
log.Fatal(err)
277+
}
278+
var namespace string
279+
if !allNamespaces {
280+
namespace = getNamespace()
281+
}
282+
283+
var store *storage.Storage
284+
switch os.Getenv("HELM_DRIVER") {
285+
case "secret", "secrets", "":
286+
d := driver.NewSecrets(clientset.CoreV1().Secrets(namespace))
287+
d.Log = debug
288+
store = storage.Init(d)
289+
case "configmap", "configmaps":
290+
d := driver.NewConfigMaps(clientset.CoreV1().ConfigMaps(namespace))
291+
d.Log = debug
292+
store = storage.Init(d)
293+
case "memory":
294+
d := driver.NewMemory()
295+
store = storage.Init(d)
296+
default:
297+
// Not sure what to do here.
298+
panic("Unknown driver in HELM_DRIVER: " + os.Getenv("HELM_DRIVER"))
299+
}
300+
301+
actionConfig.RESTClientGetter = kubeConfig()
302+
actionConfig.KubeClient = kc
303+
actionConfig.Releases = store
304+
actionConfig.Log = debug
305+
}
306+
307+
func getNamespace() string {
308+
if envSettings.Namespace != "" {
309+
return envSettings.Namespace
310+
}
311+
312+
if ns, _, err := kubeConfig().ToRawKubeConfigLoader().Namespace(); err == nil {
313+
return ns
314+
}
315+
return "default"
316+
}
317+
318+
func kubeConfig() genericclioptions.RESTClientGetter {
319+
configOnce.Do(func() {
320+
config = kube.GetConfig(settings.KubeConfig, settings.KubeContext, envSettings.Namespace)
321+
})
322+
return config
323+
}

cmd/upgrade.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import (
99
"github.com/spf13/cobra"
1010
"k8s.io/helm/pkg/helm"
1111

12+
"helm.sh/helm/pkg/action"
13+
1214
"github.com/databus23/helm-diff/diff"
1315
"github.com/databus23/helm-diff/manifest"
1416
)
@@ -18,6 +20,7 @@ type diffCmd struct {
1820
chart string
1921
chartVersion string
2022
client helm.Interface
23+
v3conf *action.Configuration
2124
detailedExitCode bool
2225
devel bool
2326
namespace string // namespace to assume the release to be installed into. Defaults to the current kube config namespace.
@@ -97,6 +100,13 @@ func newChartCommand() *cobra.Command {
97100

98101
}
99102

103+
func isHelm3() bool {
104+
// See the followings. TILLER_HOST is helm2-only plugin env
105+
// Helm 2: https://github.com/helm/helm/blob/7cad59091a9451b2aa4f95aa882ea27e6b195f98/pkg/plugin/plugin.go#L175-L192
106+
// Helm 3: https://github.com/helm/helm/blob/5cb923eecbe80d1ad76399aee234717c11931d9a/pkg/plugin/plugin.go#L221-L232
107+
return os.Getenv("TILLER_HOST") == ""
108+
}
109+
100110
func (d *diffCmd) run() error {
101111
if d.chartVersion == "" && d.devel {
102112
d.chartVersion = ">0.0.0-0"
@@ -116,6 +126,10 @@ func (d *diffCmd) run() error {
116126
return err
117127
}
118128

129+
if isHelm3() {
130+
return helm3Run(d, chartPath)
131+
}
132+
119133
releaseResponse, err := d.client.ReleaseContent(d.release)
120134

121135
var newInstall bool

0 commit comments

Comments
 (0)