github.com/pluralsh/plural-cli@v0.9.5/pkg/scaffold/helm.go (about) 1 package scaffold 2 3 import ( 4 "bytes" 5 "fmt" 6 "os" 7 "path/filepath" 8 "sort" 9 "strings" 10 ttpl "text/template" 11 12 "gopkg.in/yaml.v2" 13 14 "github.com/pluralsh/plural-cli/pkg/api" 15 "github.com/pluralsh/plural-cli/pkg/config" 16 "github.com/pluralsh/plural-cli/pkg/manifest" 17 "github.com/pluralsh/plural-cli/pkg/provider" 18 scftmpl "github.com/pluralsh/plural-cli/pkg/scaffold/template" 19 "github.com/pluralsh/plural-cli/pkg/template" 20 "github.com/pluralsh/plural-cli/pkg/utils" 21 "github.com/pluralsh/plural-cli/pkg/utils/errors" 22 "github.com/pluralsh/plural-cli/pkg/utils/git" 23 "github.com/pluralsh/plural-cli/pkg/utils/pathing" 24 "github.com/pluralsh/plural-cli/pkg/wkspace" 25 ) 26 27 type dependency struct { 28 Name string 29 Version string 30 Repository string 31 Condition string 32 } 33 34 type chart struct { 35 ApiVersion string `yaml:"apiVersion"` 36 Name string 37 Description string 38 Version string 39 AppVersion string `yaml:"appVersion"` 40 Dependencies []dependency 41 } 42 43 func (s *Scaffold) handleHelm(wk *wkspace.Workspace) error { 44 if err := s.createChart(wk); err != nil { 45 return err 46 } 47 48 if err := s.buildChartValues(wk); err != nil { 49 return err 50 } 51 52 return nil 53 } 54 55 func (s *Scaffold) chartDependencies(w *wkspace.Workspace) []dependency { 56 dependencies := make([]dependency, len(w.Charts)) 57 repo := w.Installation.Repository 58 for i, chartInstallation := range w.Charts { 59 dependencies[i] = dependency{ 60 chartInstallation.Chart.Name, 61 chartInstallation.Version.Version, 62 repoUrl(w, repo.Name, chartInstallation.Chart.Name), 63 fmt.Sprintf("%s.enabled", chartInstallation.Chart.Name), 64 } 65 } 66 sort.SliceStable(dependencies, func(i, j int) bool { 67 return dependencies[i].Name < dependencies[j].Name 68 }) 69 return dependencies 70 } 71 72 func Notes(installation *api.Installation) error { 73 repoRoot, err := git.Root() 74 if err != nil { 75 return err 76 } 77 78 if installation.Repository != nil && installation.Repository.Notes == "" { 79 return nil 80 } 81 82 context, err := manifest.ReadContext(manifest.ContextPath()) 83 if err != nil { 84 return err 85 } 86 87 prov, err := provider.GetProvider() 88 if err != nil { 89 return err 90 } 91 92 repo := installation.Repository.Name 93 ctx, _ := context.Repo(installation.Repository.Name) 94 95 vals := map[string]interface{}{ 96 "Values": ctx, 97 "Configuration": context.Configuration, 98 "License": installation.LicenseKey, 99 "OIDC": installation.OIDCProvider, 100 "Region": prov.Region(), 101 "Project": prov.Project(), 102 "Cluster": prov.Cluster(), 103 "Config": config.Read(), 104 "Provider": prov.Name(), 105 "Context": prov.Context(), 106 "Applications": BuildApplications(repoRoot), 107 } 108 109 if context.Globals != nil { 110 vals["Globals"] = context.Globals 111 } 112 113 if context.SMTP != nil { 114 vals["SMTP"] = context.SMTP.Configuration() 115 } 116 117 if installation.AcmeKeyId != "" { 118 vals["Acme"] = map[string]string{ 119 "KeyId": installation.AcmeKeyId, 120 "Secret": installation.AcmeSecret, 121 } 122 } 123 124 apps := &Applications{Root: repoRoot} 125 values, err := apps.HelmValues(repo) 126 if err != nil { 127 return err 128 } 129 130 for k, v := range values { 131 vals[k] = v 132 } 133 134 tmpl, err := template.MakeTemplate(installation.Repository.Notes) 135 if err != nil { 136 return err 137 } 138 139 var buf bytes.Buffer 140 buf.Grow(5 * 1024) 141 if err := tmpl.Execute(&buf, vals); err != nil { 142 return err 143 } 144 145 fmt.Println(buf.String()) 146 return nil 147 } 148 149 func (s *Scaffold) buildChartValues(w *wkspace.Workspace) error { 150 ctx, _ := w.Context.Repo(w.Installation.Repository.Name) 151 valuesFile := pathing.SanitizeFilepath(filepath.Join(s.Root, "values.yaml")) 152 defaultValuesFile := pathing.SanitizeFilepath(filepath.Join(s.Root, "default-values.yaml")) 153 defaultPrevVals, _ := prevValues(defaultValuesFile) 154 prevVals, _ := prevValues(valuesFile) 155 156 if !utils.Exists(valuesFile) { 157 if err := os.WriteFile(valuesFile, []byte("{}\n"), 0644); err != nil { 158 return err 159 } 160 } 161 162 conf := config.Read() 163 164 apps, err := NewApplications() 165 if err != nil { 166 return err 167 } 168 169 proj, err := manifest.FetchProject() 170 if err != nil { 171 return err 172 } 173 174 vals := map[string]interface{}{ 175 "Values": ctx, 176 "Configuration": w.Context.Configuration, 177 "License": w.Installation.LicenseKey, 178 "OIDC": w.Installation.OIDCProvider, 179 "Region": w.Provider.Region(), 180 "Project": w.Provider.Project(), 181 "Cluster": w.Provider.Cluster(), 182 "Config": conf, 183 "Provider": w.Provider.Name(), 184 "Context": w.Provider.Context(), 185 "ClusterAPI": proj.ClusterAPI, 186 "Network": proj.Network, 187 "Applications": apps, 188 } 189 190 if proj.AvailabilityZones != nil { 191 vals["AvailabilityZones"] = proj.AvailabilityZones 192 } 193 194 if w.Context.SMTP != nil { 195 vals["SMTP"] = w.Context.SMTP.Configuration() 196 } 197 198 if w.Context.Globals != nil { 199 vals["Globals"] = w.Context.Globals 200 } 201 202 if w.Installation.AcmeKeyId != "" { 203 vals["Acme"] = map[string]string{ 204 "KeyId": w.Installation.AcmeKeyId, 205 "Secret": w.Installation.AcmeSecret, 206 } 207 } 208 209 // get previous values from default-values.yaml if exists otherwise from values.yaml 210 if utils.Exists(defaultValuesFile) { 211 for k, v := range defaultPrevVals { 212 vals[k] = v 213 } 214 } else { 215 for k, v := range prevVals { 216 vals[k] = v 217 } 218 } 219 defaultValues, err := scftmpl.BuildValuesFromTemplate(vals, w) 220 if err != nil { 221 return err 222 } 223 224 io, err := yaml.Marshal(defaultValues) 225 if err != nil { 226 return err 227 } 228 229 // TODO: Remove this after testing. It is deprecated as values.yaml migration should not longer be required. 230 // mapValues, err := getValues(valuesFile) 231 // if err != nil { 232 // return err 233 // } 234 // patchValues, err := utils.PatchInterfaceMap(defaultValues, mapValues) 235 // if err != nil { 236 // return err 237 // } 238 // 239 // values, err := yaml.Marshal(patchValues) 240 // if err != nil { 241 // return err 242 // } 243 // if err := utils.WriteFile(valuesFile, values); err != nil { 244 // return err 245 // } 246 247 return utils.WriteFile(defaultValuesFile, io) 248 } 249 250 //nolint:golint,unused 251 func getValues(path string) (map[string]map[string]interface{}, error) { 252 values := map[string]map[string]interface{}{} 253 valuesFromFile, err := os.ReadFile(path) 254 if err != nil { 255 return nil, err 256 } 257 if err := yaml.Unmarshal(valuesFromFile, &values); err != nil { 258 return nil, err 259 } 260 return values, nil 261 } 262 263 func prevValues(filename string) (map[string]map[string]interface{}, error) { 264 vals := make(map[string]map[interface{}]interface{}) 265 parsed := make(map[string]map[string]interface{}) 266 if !utils.Exists(filename) { 267 return parsed, nil 268 } 269 270 contents, err := os.ReadFile(filename) 271 if err != nil { 272 return parsed, err 273 } 274 if err := yaml.Unmarshal(contents, &vals); err != nil { 275 return parsed, err 276 } 277 278 for k, v := range vals { 279 parsed[k] = utils.CleanUpInterfaceMap(v) 280 } 281 282 return parsed, nil 283 } 284 285 func (s *Scaffold) createChart(w *wkspace.Workspace) error { 286 repo := w.Installation.Repository 287 if len(w.Charts) == 0 { 288 return utils.HighlightError(fmt.Errorf("No charts installed for this repository. You might need to run `plural bundle install %s <bundle-name>`.", repo.Name)) 289 } 290 291 version := "0.1.0" 292 filename := pathing.SanitizeFilepath(filepath.Join(s.Root, ChartfileName)) 293 294 if utils.Exists(filename) { 295 content, err := os.ReadFile(filename) 296 if err != nil { 297 return errors.ErrorWrap(err, "Failed to read existing Chart.yaml") 298 } 299 300 chart := chart{} 301 if err := yaml.Unmarshal(content, &chart); err != nil { 302 return errors.ErrorWrap(err, "Existing Chart.yaml has invalid yaml formatting") 303 } 304 305 version = chart.Version 306 } 307 308 appVersion := appVersion(w.Charts) 309 chart := &chart{ 310 ApiVersion: "v2", 311 Name: repo.Name, 312 Description: fmt.Sprintf("A helm chart for %s", repo.Name), 313 Version: version, 314 AppVersion: appVersion, 315 Dependencies: s.chartDependencies(w), 316 } 317 318 chartFile, err := yaml.Marshal(chart) 319 if err != nil { 320 return err 321 } 322 323 if err := utils.WriteFile(filename, chartFile); err != nil { 324 return err 325 } 326 327 files := []struct { 328 path string 329 content []byte 330 force bool 331 }{ 332 { 333 // .helmignore 334 path: pathing.SanitizeFilepath(filepath.Join(s.Root, IgnorefileName)), 335 content: []byte(defaultIgnore), 336 }, 337 { 338 // NOTES.txt 339 path: pathing.SanitizeFilepath(filepath.Join(s.Root, NotesName)), 340 content: []byte(defaultNotes), 341 force: true, 342 }, 343 { 344 // templates/secret.yaml 345 path: pathing.SanitizeFilepath(filepath.Join(s.Root, LicenseSecretName)), 346 content: []byte(licenseSecret), 347 force: true, 348 }, 349 { 350 // templates/licnse.yaml 351 path: pathing.SanitizeFilepath(filepath.Join(s.Root, LicenseCrdName)), 352 content: []byte(fmt.Sprintf(license, repo.Name)), 353 force: true, 354 }, 355 } 356 357 for _, file := range files { 358 if !file.force { 359 if _, err := os.Stat(file.path); err == nil { 360 // File exists and is okay. Skip it. 361 continue 362 } 363 } 364 if err := utils.WriteFile(file.path, file.content); err != nil { 365 return err 366 } 367 } 368 369 // remove old requirements.yaml files to fully migrate to helm v3 370 reqsFile := pathing.SanitizeFilepath(filepath.Join(s.Root, "requirements.yaml")) 371 if utils.Exists(reqsFile) { 372 if err := os.Remove(reqsFile); err != nil { 373 return err 374 } 375 } 376 377 tpl, err := ttpl.New("gotpl").Parse(defaultApplication) 378 if err != nil { 379 return err 380 } 381 382 var appBuffer bytes.Buffer 383 vars := map[string]string{ 384 "Name": repo.Name, 385 "Version": appVersion, 386 "Description": repo.Description, 387 "Icon": repo.Icon, 388 "DarkIcon": repo.DarkIcon, 389 } 390 if err := tpl.Execute(&appBuffer, vars); err != nil { 391 return err 392 } 393 appBuffer.WriteString(appTemplate) 394 395 if err := utils.WriteFile(pathing.SanitizeFilepath(filepath.Join(s.Root, ApplicationName)), appBuffer.Bytes()); err != nil { 396 return err 397 } 398 399 // Need to add the ChartsDir explicitly as it does not contain any file OOTB 400 if err := os.MkdirAll(pathing.SanitizeFilepath(filepath.Join(s.Root, ChartsDir)), 0755); err != nil { 401 return err 402 } 403 404 return nil 405 } 406 407 func repoUrl(w *wkspace.Workspace, repo string, chart string) string { 408 if w.Links != nil { 409 if path, ok := w.Links.Helm[chart]; ok { 410 return fmt.Sprintf("file://%s", path) 411 } 412 } 413 url := strings.ReplaceAll(w.Config.BaseUrl(), "https", "cm") 414 return fmt.Sprintf("%s/cm/%s", url, repo) 415 } 416 417 func appVersion(charts []*api.ChartInstallation) string { 418 for _, inst := range charts { 419 if inst.Chart.Dependencies.Application { 420 if inst.Version.Helm != nil { 421 if vsn, ok := inst.Version.Helm["appVersion"]; ok { 422 if v, ok := vsn.(string); ok { 423 return v 424 } 425 } 426 } 427 return inst.Version.Version 428 } 429 } 430 431 return "0.1.0" 432 }