github.com/olli-ai/jx/v2@v2.0.400-0.20210921045218-14731b4dd448/pkg/cmd/step/create/helmfile/create_helmfile.go (about) 1 package helmfile 2 3 import ( 4 "fmt" 5 "io/ioutil" 6 "net/url" 7 "os" 8 "path" 9 10 "github.com/olli-ai/jx/v2/pkg/config" 11 helmfile2 "github.com/olli-ai/jx/v2/pkg/helmfile" 12 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 13 14 "github.com/google/uuid" 15 "github.com/olli-ai/jx/v2/pkg/util" 16 17 "github.com/ghodss/yaml" 18 19 "github.com/olli-ai/jx/v2/pkg/cmd/create/options" 20 "github.com/olli-ai/jx/v2/pkg/cmd/helper" 21 "github.com/olli-ai/jx/v2/pkg/cmd/opts" 22 "github.com/olli-ai/jx/v2/pkg/cmd/templates" 23 "github.com/pkg/errors" 24 "github.com/spf13/cobra" 25 ) 26 27 const ( 28 helmfile = "helmfile.yaml" 29 ) 30 31 var ( 32 createHelmfileLong = templates.LongDesc(` 33 Creates a new helmfile.yaml from a jx-apps.yaml 34 `) 35 36 createHelmfileExample = templates.Examples(` 37 # Create a new helmfile.yaml from a jx-apps.yaml 38 jx create helmfile 39 `) 40 ) 41 42 // GeneratedValues is a struct that gets marshalled into helm values for creating namespaces via helm 43 type GeneratedValues struct { 44 Namespaces []string `json:"namespaces"` 45 } 46 47 // CreateHelmfileOptions the options for the create helmfile command 48 type CreateHelmfileOptions struct { 49 options.CreateOptions 50 51 dir string 52 outputDir string 53 valueFiles []string 54 } 55 56 // NewCmdCreateHelmfile creates a command object for the "create" command 57 func NewCmdCreateHelmfile(commonOpts *opts.CommonOptions) *cobra.Command { 58 o := &CreateHelmfileOptions{ 59 CreateOptions: options.CreateOptions{ 60 CommonOptions: commonOpts, 61 }, 62 } 63 64 cmd := &cobra.Command{ 65 Use: "helmfile", 66 Short: "Create a new helmfile", 67 Long: createHelmfileLong, 68 Example: createHelmfileExample, 69 Run: func(cmd *cobra.Command, args []string) { 70 o.Cmd = cmd 71 o.Args = args 72 err := o.Run() 73 helper.CheckErr(err) 74 }, 75 } 76 cmd.Flags().StringVarP(&o.dir, "dir", "", ".", "the directory to look for a 'jx-apps.yml' file") 77 cmd.Flags().StringVarP(&o.outputDir, "outputDir", "", "", "The directory to write the helmfile.yaml file") 78 cmd.Flags().StringArrayVarP(&o.valueFiles, "values", "", []string{""}, "specify values in a YAML file or a URL(can specify multiple)") 79 80 return cmd 81 } 82 83 // Run implements the command 84 func (o *CreateHelmfileOptions) Run() error { 85 86 apps, err := config.LoadApplicationsConfig(o.dir) 87 if err != nil { 88 return errors.Wrap(err, "failed to load applications") 89 } 90 91 helm := o.Helm() 92 localHelmRepos, err := helm.ListRepos() 93 if err != nil { 94 return errors.Wrap(err, "failed listing helm repos") 95 } 96 97 // iterate over all apps and split them into phases to generate separate helmfiles for each 98 var applications []config.Application 99 var systemApplications []config.Application 100 for _, app := range apps.Applications { 101 // default phase is apps so set it in if empty 102 if app.Phase == "" || app.Phase == config.PhaseApps { 103 applications = append(applications, app) 104 } 105 if app.Phase == config.PhaseSystem { 106 systemApplications = append(systemApplications, app) 107 } 108 } 109 110 err = o.generateHelmFile(applications, err, localHelmRepos, apps, string(config.PhaseApps)) 111 if err != nil { 112 return errors.Wrap(err, "failed to generate apps helmfile") 113 } 114 err = o.generateHelmFile(systemApplications, err, localHelmRepos, apps, string(config.PhaseSystem)) 115 if err != nil { 116 return errors.Wrap(err, "failed to generate system helmfile") 117 } 118 119 return nil 120 } 121 122 func (o *CreateHelmfileOptions) generateHelmFile(applications []config.Application, err error, localHelmRepos map[string]string, apps *config.ApplicationConfig, phase string) error { 123 // contains the repo url and name to reference it by in the release spec 124 // use a map to dedupe repositories 125 repos := make(map[string]string) 126 for _, app := range applications { 127 _, err = url.ParseRequestURI(app.Repository) 128 if err != nil { 129 // 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 130 repos[app.Repository] = app.Repository 131 } else { 132 matched := false 133 // check if URL matches a repo in helms local list 134 for key, value := range localHelmRepos { 135 if app.Repository == value { 136 repos[app.Repository] = key 137 matched = true 138 } 139 } 140 if !matched { 141 repos[app.Repository] = uuid.New().String() 142 } 143 } 144 } 145 var repositories []helmfile2.RepositorySpec 146 var releases []helmfile2.ReleaseSpec 147 for repoURL, name := range repos { 148 _, err = url.ParseRequestURI(repoURL) 149 // skip non URLs as they're probably local directories which don't need to be in the helmfile.repository section 150 if err == nil { 151 repository := helmfile2.RepositorySpec{ 152 Name: name, 153 URL: repoURL, 154 } 155 repositories = append(repositories, repository) 156 } 157 } 158 for _, app := range applications { 159 160 if app.Namespace == "" { 161 app.Namespace = apps.DefaultNamespace 162 } 163 164 // check if a local directory and values file exists for the app 165 extraValuesFiles := o.valueFiles 166 extraValuesFiles = o.addExtraAppValues(app, extraValuesFiles, "values.yaml", phase) 167 extraValuesFiles = o.addExtraAppValues(app, extraValuesFiles, "values.yaml.gotmpl", phase) 168 169 chartName := fmt.Sprintf("%s/%s", repos[app.Repository], app.Name) 170 release := helmfile2.ReleaseSpec{ 171 Name: app.Name, 172 Namespace: app.Namespace, 173 Chart: chartName, 174 Values: extraValuesFiles, 175 } 176 releases = append(releases, release) 177 } 178 179 // ensure any namespaces referenced are created first, do this via an extra chart that creates namespaces 180 // so that helm manages the k8s resources, useful when cleaning up, this is a workaround for a helm 3 limitation 181 // which is expected to be fixed 182 repositories, releases, err = o.ensureNamespaceExist(repositories, releases, phase) 183 if err != nil { 184 return errors.Wrapf(err, "failed to check namespaces exists") 185 } 186 h := helmfile2.HelmState{ 187 Bases: []string{"../environments.yaml"}, 188 HelmDefaults: helmfile2.HelmSpec{ 189 Atomic: true, 190 Verify: false, 191 Wait: true, 192 Timeout: 180, 193 // need Force to be false https://github.com/helm/helm/issues/6378 194 Force: false, 195 }, 196 Repositories: repositories, 197 Releases: releases, 198 } 199 data, err := yaml.Marshal(h) 200 if err != nil { 201 return errors.Wrapf(err, "failed to marshal helmfile data") 202 } 203 204 err = o.writeHelmfile(err, phase, data) 205 if err != nil { 206 return errors.Wrapf(err, "failed to write helmfile") 207 } 208 return nil 209 } 210 211 func (o *CreateHelmfileOptions) writeHelmfile(err error, phase string, data []byte) error { 212 exists, err := util.DirExists(path.Join(o.outputDir, phase)) 213 if err != nil || !exists { 214 err = os.MkdirAll(path.Join(o.outputDir, phase), os.ModePerm) 215 if err != nil { 216 return errors.Wrapf(err, "cannot create phase directory %s ", path.Join(o.outputDir, phase)) 217 } 218 } 219 err = ioutil.WriteFile(path.Join(o.outputDir, phase, helmfile), data, util.DefaultWritePermissions) 220 if err != nil { 221 return errors.Wrapf(err, "failed to save file %s", helmfile) 222 } 223 return nil 224 } 225 226 func (o *CreateHelmfileOptions) addExtraAppValues(app config.Application, newValuesFiles []string, valuesFilename, phase string) []string { 227 fileName := path.Join(o.dir, phase, app.Name, valuesFilename) 228 exists, _ := util.FileExists(fileName) 229 if exists { 230 newValuesFiles = append(newValuesFiles, path.Join(app.Name, valuesFilename)) 231 } 232 return newValuesFiles 233 } 234 235 // this is a temporary function that wont be needed once helm 3 supports creating namespaces 236 func (o *CreateHelmfileOptions) ensureNamespaceExist(helmfileRepos []helmfile2.RepositorySpec, helmfileReleases []helmfile2.ReleaseSpec, phase string) ([]helmfile2.RepositorySpec, []helmfile2.ReleaseSpec, error) { 237 238 // start by deleting the existing generated directory 239 err := os.RemoveAll(path.Join(o.outputDir, phase, "generated")) 240 if err != nil { 241 return nil, nil, errors.Wrapf(err, "cannot delete generated values directory %s ", path.Join(phase, "generated")) 242 } 243 244 client, currentNamespace, err := o.KubeClientAndNamespace() 245 if err != nil { 246 return nil, nil, errors.Wrapf(err, "failed to create kube client") 247 } 248 249 namespaces, err := client.CoreV1().Namespaces().List(metav1.ListOptions{}) 250 if err != nil { 251 return nil, nil, errors.Wrapf(err, "failed to list namespaces") 252 } 253 254 namespaceMatched := false 255 // loop over each application and check if the namespace it references exists, if not add the namespace creator chart to the helmfile 256 for k, release := range helmfileReleases { 257 for _, ns := range namespaces.Items { 258 if ns.Name == release.Namespace { 259 namespaceMatched = true 260 } 261 } 262 if !namespaceMatched { 263 existingCreateNamespaceChartFound := false 264 for _, release := range helmfileReleases { 265 if release.Name == "namespace-"+release.Namespace { 266 existingCreateNamespaceChartFound = true 267 } 268 } 269 if !existingCreateNamespaceChartFound { 270 271 err := o.writeGeneratedNamespaceValues(release.Namespace, phase) 272 if err != nil { 273 return nil, nil, errors.Wrapf(err, "failed to write generated namespace values file") 274 } 275 276 repository := helmfile2.RepositorySpec{ 277 Name: "zloeber", 278 URL: "git+https://github.com/zloeber/helm-namespace@chart", 279 } 280 helmfileRepos = append(helmfileRepos, repository) 281 282 createNamespaceChart := helmfile2.ReleaseSpec{ 283 Name: "namespace-" + release.Namespace, 284 Namespace: currentNamespace, 285 Chart: "zloeber/namespace", 286 287 Values: []string{path.Join("generated", release.Namespace, "values.yaml")}, 288 } 289 290 // add a dependency so that the create namespace chart is installed before the app chart 291 helmfileReleases[k].Needs = []string{fmt.Sprintf("%s/namespace-%s", currentNamespace, release.Namespace)} 292 293 helmfileReleases = append(helmfileReleases, createNamespaceChart) 294 } 295 } 296 } 297 298 return helmfileRepos, helmfileReleases, nil 299 } 300 301 func (o *CreateHelmfileOptions) writeGeneratedNamespaceValues(namespace, phase string) error { 302 // workaround with using []interface{} for values, this causes problems with (un)marshalling so lets write a file and 303 // add the file path to the []string values 304 err := os.MkdirAll(path.Join(o.outputDir, phase, "generated", namespace), os.ModePerm) 305 if err != nil { 306 return errors.Wrapf(err, "cannot create generated values directory %s ", path.Join(phase, "generated", namespace)) 307 } 308 value := GeneratedValues{ 309 Namespaces: []string{namespace}, 310 } 311 data, err := yaml.Marshal(value) 312 if err != nil { 313 return err 314 } 315 err = ioutil.WriteFile(path.Join(o.outputDir, phase, "generated", namespace, "values.yaml"), data, util.DefaultWritePermissions) 316 return nil 317 }