/
create_helmfile.go
317 lines (276 loc) · 10.2 KB
/
create_helmfile.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
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
package helmfile
import (
"fmt"
"io/ioutil"
"net/url"
"os"
"path"
"github.com/jenkins-x/jx/v2/pkg/config"
helmfile2 "github.com/jenkins-x/jx/v2/pkg/helmfile"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"github.com/google/uuid"
"github.com/jenkins-x/jx/v2/pkg/util"
"github.com/ghodss/yaml"
"github.com/jenkins-x/jx/v2/pkg/cmd/create/options"
"github.com/jenkins-x/jx/v2/pkg/cmd/helper"
"github.com/jenkins-x/jx/v2/pkg/cmd/opts"
"github.com/jenkins-x/jx/v2/pkg/cmd/templates"
"github.com/pkg/errors"
"github.com/spf13/cobra"
)
const (
helmfile = "helmfile.yaml"
)
var (
createHelmfileLong = templates.LongDesc(`
Creates a new helmfile.yaml from a jx-apps.yaml
`)
createHelmfileExample = templates.Examples(`
# Create a new helmfile.yaml from a jx-apps.yaml
jx create helmfile
`)
)
// GeneratedValues is a struct that gets marshalled into helm values for creating namespaces via helm
type GeneratedValues struct {
Namespaces []string `json:"namespaces"`
}
// CreateHelmfileOptions the options for the create helmfile command
type CreateHelmfileOptions struct {
options.CreateOptions
dir string
outputDir string
valueFiles []string
}
// NewCmdCreateHelmfile creates a command object for the "create" command
func NewCmdCreateHelmfile(commonOpts *opts.CommonOptions) *cobra.Command {
o := &CreateHelmfileOptions{
CreateOptions: options.CreateOptions{
CommonOptions: commonOpts,
},
}
cmd := &cobra.Command{
Use: "helmfile",
Short: "Create a new helmfile",
Long: createHelmfileLong,
Example: createHelmfileExample,
Run: func(cmd *cobra.Command, args []string) {
o.Cmd = cmd
o.Args = args
err := o.Run()
helper.CheckErr(err)
},
}
cmd.Flags().StringVarP(&o.dir, "dir", "", ".", "the directory to look for a 'jx-apps.yml' file")
cmd.Flags().StringVarP(&o.outputDir, "outputDir", "", "", "The directory to write the helmfile.yaml file")
cmd.Flags().StringArrayVarP(&o.valueFiles, "values", "", []string{""}, "specify values in a YAML file or a URL(can specify multiple)")
return cmd
}
// Run implements the command
func (o *CreateHelmfileOptions) Run() error {
apps, err := config.LoadApplicationsConfig(o.dir)
if err != nil {
return errors.Wrap(err, "failed to load applications")
}
helm := o.Helm()
localHelmRepos, err := helm.ListRepos()
if err != nil {
return errors.Wrap(err, "failed listing helm repos")
}
// iterate over all apps and split them into phases to generate separate helmfiles for each
var applications []config.Application
var systemApplications []config.Application
for _, app := range apps.Applications {
// default phase is apps so set it in if empty
if app.Phase == "" || app.Phase == config.PhaseApps {
applications = append(applications, app)
}
if app.Phase == config.PhaseSystem {
systemApplications = append(systemApplications, app)
}
}
err = o.generateHelmFile(applications, err, localHelmRepos, apps, string(config.PhaseApps))
if err != nil {
return errors.Wrap(err, "failed to generate apps helmfile")
}
err = o.generateHelmFile(systemApplications, err, localHelmRepos, apps, string(config.PhaseSystem))
if err != nil {
return errors.Wrap(err, "failed to generate system helmfile")
}
return nil
}
func (o *CreateHelmfileOptions) generateHelmFile(applications []config.Application, err error, localHelmRepos map[string]string, apps *config.ApplicationConfig, phase string) error {
// contains the repo url and name to reference it by in the release spec
// use a map to dedupe repositories
repos := make(map[string]string)
for _, app := range applications {
_, err = url.ParseRequestURI(app.Repository)
if err != nil {
// if the repository isn't a valid URL lets just use whatever was supplied in the application repository field, probably it is a directory path
repos[app.Repository] = app.Repository
} else {
matched := false
// check if URL matches a repo in helms local list
for key, value := range localHelmRepos {
if app.Repository == value {
repos[app.Repository] = key
matched = true
}
}
if !matched {
repos[app.Repository] = uuid.New().String()
}
}
}
var repositories []helmfile2.RepositorySpec
var releases []helmfile2.ReleaseSpec
for repoURL, name := range repos {
_, err = url.ParseRequestURI(repoURL)
// skip non URLs as they're probably local directories which don't need to be in the helmfile.repository section
if err == nil {
repository := helmfile2.RepositorySpec{
Name: name,
URL: repoURL,
}
repositories = append(repositories, repository)
}
}
for _, app := range applications {
if app.Namespace == "" {
app.Namespace = apps.DefaultNamespace
}
// check if a local directory and values file exists for the app
extraValuesFiles := o.valueFiles
extraValuesFiles = o.addExtraAppValues(app, extraValuesFiles, "values.yaml", phase)
extraValuesFiles = o.addExtraAppValues(app, extraValuesFiles, "values.yaml.gotmpl", phase)
chartName := fmt.Sprintf("%s/%s", repos[app.Repository], app.Name)
release := helmfile2.ReleaseSpec{
Name: app.Name,
Namespace: app.Namespace,
Chart: chartName,
Values: extraValuesFiles,
}
releases = append(releases, release)
}
// ensure any namespaces referenced are created first, do this via an extra chart that creates namespaces
// so that helm manages the k8s resources, useful when cleaning up, this is a workaround for a helm 3 limitation
// which is expected to be fixed
repositories, releases, err = o.ensureNamespaceExist(repositories, releases, phase)
if err != nil {
return errors.Wrapf(err, "failed to check namespaces exists")
}
h := helmfile2.HelmState{
Bases: []string{"../environments.yaml"},
HelmDefaults: helmfile2.HelmSpec{
Atomic: true,
Verify: false,
Wait: true,
Timeout: 180,
// need Force to be false https://github.com/helm/helm/issues/6378
Force: false,
},
Repositories: repositories,
Releases: releases,
}
data, err := yaml.Marshal(h)
if err != nil {
return errors.Wrapf(err, "failed to marshal helmfile data")
}
err = o.writeHelmfile(err, phase, data)
if err != nil {
return errors.Wrapf(err, "failed to write helmfile")
}
return nil
}
func (o *CreateHelmfileOptions) writeHelmfile(err error, phase string, data []byte) error {
exists, err := util.DirExists(path.Join(o.outputDir, phase))
if err != nil || !exists {
err = os.MkdirAll(path.Join(o.outputDir, phase), os.ModePerm)
if err != nil {
return errors.Wrapf(err, "cannot create phase directory %s ", path.Join(o.outputDir, phase))
}
}
err = ioutil.WriteFile(path.Join(o.outputDir, phase, helmfile), data, util.DefaultWritePermissions)
if err != nil {
return errors.Wrapf(err, "failed to save file %s", helmfile)
}
return nil
}
func (o *CreateHelmfileOptions) addExtraAppValues(app config.Application, newValuesFiles []string, valuesFilename, phase string) []string {
fileName := path.Join(o.dir, phase, app.Name, valuesFilename)
exists, _ := util.FileExists(fileName)
if exists {
newValuesFiles = append(newValuesFiles, path.Join(app.Name, valuesFilename))
}
return newValuesFiles
}
// this is a temporary function that wont be needed once helm 3 supports creating namespaces
func (o *CreateHelmfileOptions) ensureNamespaceExist(helmfileRepos []helmfile2.RepositorySpec, helmfileReleases []helmfile2.ReleaseSpec, phase string) ([]helmfile2.RepositorySpec, []helmfile2.ReleaseSpec, error) {
// start by deleting the existing generated directory
err := os.RemoveAll(path.Join(o.outputDir, phase, "generated"))
if err != nil {
return nil, nil, errors.Wrapf(err, "cannot delete generated values directory %s ", path.Join(phase, "generated"))
}
client, currentNamespace, err := o.KubeClientAndNamespace()
if err != nil {
return nil, nil, errors.Wrapf(err, "failed to create kube client")
}
namespaces, err := client.CoreV1().Namespaces().List(metav1.ListOptions{})
if err != nil {
return nil, nil, errors.Wrapf(err, "failed to list namespaces")
}
namespaceMatched := false
// loop over each application and check if the namespace it references exists, if not add the namespace creator chart to the helmfile
for k, release := range helmfileReleases {
for _, ns := range namespaces.Items {
if ns.Name == release.Namespace {
namespaceMatched = true
}
}
if !namespaceMatched {
existingCreateNamespaceChartFound := false
for _, release := range helmfileReleases {
if release.Name == "namespace-"+release.Namespace {
existingCreateNamespaceChartFound = true
}
}
if !existingCreateNamespaceChartFound {
err := o.writeGeneratedNamespaceValues(release.Namespace, phase)
if err != nil {
return nil, nil, errors.Wrapf(err, "failed to write generated namespace values file")
}
repository := helmfile2.RepositorySpec{
Name: "zloeber",
URL: "git+https://github.com/zloeber/helm-namespace@chart",
}
helmfileRepos = append(helmfileRepos, repository)
createNamespaceChart := helmfile2.ReleaseSpec{
Name: "namespace-" + release.Namespace,
Namespace: currentNamespace,
Chart: "zloeber/namespace",
Values: []string{path.Join("generated", release.Namespace, "values.yaml")},
}
// add a dependency so that the create namespace chart is installed before the app chart
helmfileReleases[k].Needs = []string{fmt.Sprintf("%s/namespace-%s", currentNamespace, release.Namespace)}
helmfileReleases = append(helmfileReleases, createNamespaceChart)
}
}
}
return helmfileRepos, helmfileReleases, nil
}
func (o *CreateHelmfileOptions) writeGeneratedNamespaceValues(namespace, phase string) error {
// workaround with using []interface{} for values, this causes problems with (un)marshalling so lets write a file and
// add the file path to the []string values
err := os.MkdirAll(path.Join(o.outputDir, phase, "generated", namespace), os.ModePerm)
if err != nil {
return errors.Wrapf(err, "cannot create generated values directory %s ", path.Join(phase, "generated", namespace))
}
value := GeneratedValues{
Namespaces: []string{namespace},
}
data, err := yaml.Marshal(value)
if err != nil {
return err
}
err = ioutil.WriteFile(path.Join(o.outputDir, phase, "generated", namespace, "values.yaml"), data, util.DefaultWritePermissions)
return nil
}