forked from burdiyan/helm-update-config
-
Notifications
You must be signed in to change notification settings - Fork 3
/
Copy pathupdatecfg.go
278 lines (228 loc) · 7.24 KB
/
updatecfg.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
package main
import (
"fmt"
"io/ioutil"
"os"
"path/filepath"
"strings"
"github.com/pkg/errors"
"github.com/spf13/cobra"
flag "github.com/spf13/pflag"
"gopkg.in/yaml.v1"
"k8s.io/client-go/util/homedir"
"k8s.io/helm/pkg/helm"
helmEnv "k8s.io/helm/pkg/helm/environment"
"k8s.io/helm/pkg/strvals"
"k8s.io/helm/pkg/tlsutil"
)
//const (
// // DefaultTLSCaCert is the default value for HELM_TLS_CA_CERT
// DefaultTLSCaCert = "$HELM_HOME/ca.pem"
// // DefaultTLSCert is the default value for HELM_TLS_CERT
// DefaultTLSCert = "$HELM_HOME/cert.pem"
// // DefaultTLSKeyFile is the default value for HELM_TLS_KEY_FILE
// DefaultTLSKeyFile = "$HELM_HOME/key.pem"
// // DefaultTLSEnable is the default value for HELM_TLS_ENABLE
// DefaultTLSEnable = false
// // DefaultTLSVerify is the default value for HELM_TLS_VERIFY
// DefaultTLSVerify = false
//)
var (
settings helmEnv.EnvSettings
DefaultHelmHome = filepath.Join(homedir.HomeDir(), ".helm")
)
func addCommonCmdOptions(f *flag.FlagSet) {
settings.AddFlagsTLS(f)
settings.InitTLS(f)
f.StringVar((*string)(&settings.Home), "home", DefaultHelmHome, "location of your Helm config. Overrides $HELM_HOME")
}
type cmdFlags struct {
cliValues []string
valueFiles ValueFiles
// TillerHost is the host and port of Tiller.
TillerHost string
// TLSEnable tells helm to communicate with Tiller via TLS
TLSEnable bool
// TLSVerify tells helm to communicate with Tiller via TLS and to verify remote certificates served by Tiller
TLSVerify bool
// TLSServerName tells helm to verify the hostname on the returned certificates from Tiller
TLSServerName string
// TLSCaCertFile is the path to a TLS CA certificate file
TLSCaCertFile string
// TLSCertFile is the path to a TLS certificate file
TLSCertFile string
// TLSKeyFile is the path to a TLS key file
TLSKeyFile string
}
type ValueFiles []string
func (v *ValueFiles) String() string {
return fmt.Sprint(*v)
}
func (v *ValueFiles) Type() string {
return "valueFiles"
}
func (v *ValueFiles) Set(value string) error {
for _, filePath := range strings.Split(value, ",") {
*v = append(*v, filePath)
}
return nil
}
func createHelmClient() helm.Interface {
options := []helm.Option{helm.Host(os.Getenv("TILLER_HOST")), helm.ConnectTimeout(int64(30))}
if settings.TLSVerify || settings.TLSEnable {
tlsopts := tlsutil.Options{
ServerName: settings.TLSServerName,
KeyFile: settings.TLSKeyFile,
CertFile: settings.TLSCertFile,
InsecureSkipVerify: true,
}
if settings.TLSVerify {
tlsopts.CaCertFile = settings.TLSCaCertFile
tlsopts.InsecureSkipVerify = false
}
tlscfg, err := tlsutil.ClientConfig(tlsopts)
if err != nil {
_, _ = fmt.Fprintln(os.Stderr, err)
os.Exit(2)
}
options = append(options, helm.WithTLS(tlscfg))
}
return helm.NewClient(options...)
}
func isHelm3() bool {
return os.Getenv("TILLER_HOST") == ""
}
func newUpdatecfgCmd() *cobra.Command {
var flags cmdFlags
cmd := &cobra.Command{
Use: "helm update-config [flags] RELEASE",
Short: "update config values of an existing release",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
vals := make(map[string]interface{})
for _, v := range flags.cliValues {
if err := strvals.ParseInto(v, vals); err != nil {
return err
}
}
update := updateConfigCommand{
client: createHelmClient(),
release: args[0],
values: flags.cliValues,
valueFiles: flags.valueFiles,
useTLS: flags.TLSEnable,
}
return update.run()
},
}
f := cmd.Flags()
f.StringArrayVar(&flags.cliValues, "set-value", []string{}, "set values on the command line (can specify multiple or separate values with commas: key1=val1,key2=val2)")
f.VarP(&flags.valueFiles, "values", "f", "specify values in a YAML file")
if !isHelm3() {
addCommonCmdOptions(f)
}
return cmd
}
type updateConfigCommand struct {
client helm.Interface
release string
values []string
valueFiles ValueFiles
useTLS bool
}
func (cmd *updateConfigCommand) run() error {
// This code supports to check a release from release name directly, no limitation on naming.
ls, err := cmd.client.ListReleases(helm.ReleaseListFilter(cmd.release))
if err != nil {
return err
}
if ls.GetCount() == 0 {
return fmt.Errorf("the count of release %s is zero", cmd.release)
}
var preVals map[string]interface{}
err = yaml.Unmarshal([]byte(ls.Releases[0].Config.Raw), &preVals)
if err != nil {
return errors.Wrapf(err, "Failed to unmarshal raw values: %v", ls.Releases[0].Config.Raw)
}
preferredVals, err := GenerateUpdatedValues(cmd.valueFiles, cmd.values)
if err != nil {
return errors.Wrapf(err, "Failed to generate preferred values: %v", preferredVals)
}
mergedVals := mergeValues(preVals, preferredVals)
valBytes, err := yaml.Marshal(mergedVals)
if err != nil {
return errors.Wrapf(err, "Failed to marshal merged values: %v", mergedVals)
}
var opt helm.UpdateOption
opt = helm.ReuseValues(true)
_, err = cmd.client.UpdateReleaseFromChart(
ls.Releases[0].Name,
ls.Releases[0].Chart,
helm.UpdateValueOverrides(valBytes),
opt,
)
if err != nil {
return errors.Wrapf(err, "Failed to update release")
}
fmt.Printf("Info: update successfully\n")
return nil
}
// mergeValues merges destination and source map, preferring values from the source map
func mergeValues(dest map[string]interface{}, src map[string]interface{}) map[string]interface{} {
for k, v := range src {
// If the key doesn't exist, then just set the key to that value
if _, exists := dest[k]; !exists {
dest[k] = v
continue
}
nextMap, ok := v.(map[interface{}]interface{})
// If it isn't another map, overwrite the value
if !ok {
dest[k] = v
continue
}
// Edge case: If the key exists in the destination, but isn't a map
destMap, isMap := dest[k].(map[interface{}]interface{})
// If the source map has a map for this key, prefer it
if !isMap {
dest[k] = v
continue
}
// If they are both map, merge them
dest[k] = mergeValues(convertKeyAsString(destMap), convertKeyAsString(nextMap))
}
return dest
}
// GenerateUpdatedValues generates values from files specified via -f/--values and directly via --set-value, preferring values via --set-value
func GenerateUpdatedValues(valueFiles ValueFiles, values []string) (map[string]interface{}, error) {
base := map[string]interface{}{}
// User specified a values files via -f/--values
for _, filePath := range valueFiles {
currentMap := map[string]interface{}{}
var bytes []byte
var err error
bytes, err = ioutil.ReadFile(filePath)
if err != nil {
return map[string]interface{}{}, err
}
if err := yaml.Unmarshal(bytes, ¤tMap); err != nil {
return map[string]interface{}{}, fmt.Errorf("failed to parse %s: %s", filePath, err)
}
// Merge with the previous map
base = mergeValues(base, currentMap)
}
// User specified a value via --set-value
for _, value := range values {
if err := strvals.ParseInto(value, base); err != nil {
return map[string]interface{}{}, fmt.Errorf("failed parsing --set-value data: %s", err)
}
}
return base, nil
}
func convertKeyAsString(ori map[interface{}]interface{}) map[string]interface{} {
result := map[string]interface{}{}
for k, v := range ori {
result[k.(string)] = v
}
return result
}