github.com/olli-ai/jx/v2@v2.0.400-0.20210921045218-14731b4dd448/pkg/helm/helm_template.go (about) 1 package helm 2 3 import ( 4 "bufio" 5 "bytes" 6 "fmt" 7 "io" 8 "io/ioutil" 9 "os" 10 "path/filepath" 11 "strconv" 12 "strings" 13 "time" 14 15 "k8s.io/helm/pkg/chartutil" 16 "k8s.io/helm/pkg/proto/hapi/chart" 17 18 "github.com/jenkins-x/jx-logging/pkg/log" 19 "github.com/olli-ai/jx/v2/pkg/errorutil" 20 "github.com/olli-ai/jx/v2/pkg/kube" 21 "github.com/olli-ai/jx/v2/pkg/util" 22 "github.com/pkg/errors" 23 yaml "gopkg.in/yaml.v2" 24 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 25 "k8s.io/client-go/kubernetes" 26 ) 27 28 const ( 29 // AnnotationChartName stores the chart name 30 AnnotationChartName = "jenkins.io/chart" 31 // AnnotationAppVersion stores the chart's app version 32 AnnotationAppVersion = "jenkins.io/chart-app-version" 33 // AnnotationAppDescription stores the chart's app version 34 AnnotationAppDescription = "jenkins.io/chart-description" 35 // AnnotationAppRepository stores the chart's app repository 36 AnnotationAppRepository = "jenkins.io/chart-repository" 37 38 // LabelReleaseName stores the chart release name 39 LabelReleaseName = "jenkins.io/chart-release" 40 41 // LabelNamespace stores the chart namespace for cluster wide resources 42 LabelNamespace = "jenkins.io/namespace" 43 44 // LabelReleaseChartVersion stores the version of a chart installation in a label 45 LabelReleaseChartVersion = "jenkins.io/version" 46 // LabelAppName stores the chart's app name 47 LabelAppName = "jenkins.io/app-name" 48 // LabelAppVersion stores the chart's app version 49 LabelAppVersion = "jenkins.io/app-version" 50 51 // LabelReleaseHookChartVersion stores the version for a hook 52 LabelReleaseHookChartVersion = "jenkins.io/hook-version" 53 54 hookFailed = "hook-failed" 55 hookSucceeded = "hook-succeeded" 56 beforeHookCreation = "before-hook-creation" 57 58 // resourcesSeparator is used to separate multiple objects stored in the same YAML file 59 resourcesSeparator = "---" 60 ) 61 62 // HelmTemplate implements common helm actions but purely as client side operations 63 // delegating a separate Helmer such as HelmCLI for the client side operations 64 type HelmTemplate struct { 65 Client *HelmCLI 66 WorkDir string 67 CWD string 68 Binary string 69 Runner util.Commander 70 KubectlValidate bool 71 KubeClient kubernetes.Interface 72 Namespace string 73 } 74 75 // NewHelmTemplate creates a new HelmTemplate instance configured to the given client side Helmer 76 func NewHelmTemplate(client *HelmCLI, workDir string, kubeClient kubernetes.Interface, ns string) *HelmTemplate { 77 cli := &HelmTemplate{ 78 Client: client, 79 WorkDir: workDir, 80 Runner: client.Runner, 81 Binary: "kubectl", 82 CWD: client.CWD, 83 KubectlValidate: false, 84 KubeClient: kubeClient, 85 Namespace: ns, 86 } 87 return cli 88 } 89 90 type HelmHook struct { 91 Kind string 92 Name string 93 File string 94 Hooks []string 95 HookDeletePolicies []string 96 } 97 98 // SetHost is used to point at a locally running tiller 99 func (h *HelmTemplate) SetHost(tillerAddress string) { 100 // NOOP 101 } 102 103 // SetCWD configures the common working directory of helm CLI 104 func (h *HelmTemplate) SetCWD(dir string) { 105 h.Client.SetCWD(dir) 106 h.CWD = dir 107 } 108 109 // HelmBinary return the configured helm CLI 110 func (h *HelmTemplate) HelmBinary() string { 111 return h.Client.HelmBinary() 112 } 113 114 // SetHelmBinary configure a new helm CLI 115 func (h *HelmTemplate) SetHelmBinary(binary string) { 116 h.Client.SetHelmBinary(binary) 117 } 118 119 // Init executes the helm init command according with the given flags 120 func (h *HelmTemplate) Init(clientOnly bool, serviceAccount string, tillerNamespace string, upgrade bool) error { 121 return h.Client.Init(true, serviceAccount, tillerNamespace, upgrade) 122 } 123 124 // AddRepo adds a new helm repo with the given name and URL 125 func (h *HelmTemplate) AddRepo(repo, URL, username, password string) error { 126 return h.Client.AddRepo(repo, URL, username, password) 127 } 128 129 // RemoveRepo removes the given repo from helm 130 func (h *HelmTemplate) RemoveRepo(repo string) error { 131 return h.Client.RemoveRepo(repo) 132 } 133 134 // ListRepos list the installed helm repos together with their URL 135 func (h *HelmTemplate) ListRepos() (map[string]string, error) { 136 return h.Client.ListRepos() 137 } 138 139 // SearchCharts searches for all the charts matching the given filter 140 func (h *HelmTemplate) SearchCharts(filter string, allVersions bool) ([]ChartSummary, error) { 141 return h.Client.SearchCharts(filter, false) 142 } 143 144 // IsRepoMissing checks if the repository with the given URL is missing from helm 145 func (h *HelmTemplate) IsRepoMissing(URL string) (bool, string, error) { 146 return h.Client.IsRepoMissing(URL) 147 } 148 149 // UpdateRepo updates the helm repositories 150 func (h *HelmTemplate) UpdateRepo() error { 151 return h.Client.UpdateRepo() 152 } 153 154 // RemoveRequirementsLock removes the requirements.lock file from the current working directory 155 func (h *HelmTemplate) RemoveRequirementsLock() error { 156 return h.Client.RemoveRequirementsLock() 157 } 158 159 // BuildDependency builds the helm dependencies of the helm chart from the current working directory 160 func (h *HelmTemplate) BuildDependency() error { 161 return h.Client.BuildDependency() 162 } 163 164 // ListReleases lists the releases in ns 165 func (h *HelmTemplate) ListReleases(ns string) (map[string]ReleaseSummary, []string, error) { 166 list, err := h.KubeClient.AppsV1().Deployments(ns).List(metav1.ListOptions{}) 167 if err != nil { 168 return nil, nil, errors.WithStack(err) 169 } 170 charts := make(map[string]ReleaseSummary) 171 keys := make([]string, 0) 172 if list != nil { 173 for _, deploy := range list.Items { 174 labels := deploy.Labels 175 ann := deploy.Annotations 176 if labels != nil && ann != nil { 177 status := "ERROR" 178 if deploy.Status.Replicas > 0 { 179 if deploy.Status.UnavailableReplicas > 0 { 180 status = "PENDING" 181 } else { 182 status = "DEPLOYED" 183 } 184 } 185 updated := deploy.CreationTimestamp.Format("Mon Jan 2 15:04:05 2006") 186 chartName := ann[AnnotationChartName] 187 chartVersion := labels[LabelReleaseChartVersion] 188 releaseName := labels[LabelReleaseName] 189 keys = append(keys, releaseName) 190 charts[releaseName] = ReleaseSummary{ 191 Chart: chartName, 192 ChartFullName: chartName + "-" + chartVersion, 193 Revision: strconv.FormatInt(deploy.Generation, 10), 194 Updated: updated, 195 Status: status, 196 ChartVersion: chartVersion, 197 ReleaseName: releaseName, 198 AppVersion: ann[AnnotationAppVersion], 199 Namespace: ns, 200 } 201 } 202 } 203 } 204 return charts, keys, nil 205 } 206 207 // FindChart find a chart in the current working directory, if no chart file is found an error is returned 208 func (h *HelmTemplate) FindChart() (string, error) { 209 return h.Client.FindChart() 210 } 211 212 // Lint lints the helm chart from the current working directory and returns the warnings in the output 213 func (h *HelmTemplate) Lint(valuesFiles []string) (string, error) { 214 return h.Client.Lint(valuesFiles) 215 } 216 217 // Env returns the environment variables for the helmer 218 func (h *HelmTemplate) Env() map[string]string { 219 return h.Client.Env() 220 } 221 222 // PackageChart packages the chart from the current working directory 223 func (h *HelmTemplate) PackageChart() error { 224 return h.Client.PackageChart() 225 } 226 227 // Version executes the helm version command and returns its output 228 func (h *HelmTemplate) Version(tls bool) (string, error) { 229 return h.Client.Version(tls) 230 } 231 232 // Template generates the YAML from the chart template to the given directory 233 func (h *HelmTemplate) Template(chart string, releaseName string, ns string, outDir string, upgrade bool, values []string, valueStrings []string, 234 valueFiles []string) error { 235 236 return h.Client.Template(chart, releaseName, ns, outDir, upgrade, values, valueStrings, valueFiles) 237 } 238 239 // MultiTemplate generates the YAML from the chart template to the given directory 240 func (h *HelmTemplate) MultiTemplate(chart string, releaseName string, ns string, outDir string, upgrade bool, values []string, valueStrings []string, 241 valueFiles []string) error { 242 243 return h.Client.MultiTemplate(chart, releaseName, ns, outDir, upgrade, values, valueStrings, valueFiles) 244 } 245 246 // Mutation API 247 248 func (h *HelmTemplate) auxInstallChart(multi, create, force, wait bool, 249 chart string, releaseName string, ns string, version string, timeout int, 250 values []string, valueStrings []string, valueFiles []string, 251 repo string, username string, password string) error { 252 err := h.clearOutputDir(releaseName) 253 if err != nil { 254 return err 255 } 256 outputDir, _, chartsDir, err := h.getDirectories(releaseName) 257 if err != nil { 258 return err 259 } 260 261 // check if we are installing a chart from the filesystem 262 chartDir := filepath.Join(h.CWD, chart) 263 exists, err := util.DirExists(chartDir) 264 if err != nil { 265 return err 266 } 267 if !exists { 268 log.Logger().Debugf("Fetching chart: %s", chart) 269 chartDir, err = h.fetchChart(chart, version, chartsDir, repo, username, password) 270 if err != nil { 271 return err 272 } 273 } 274 275 if multi { 276 err = h.Client.MultiTemplate(chartDir, releaseName, ns, outputDir, false, values, valueStrings, valueFiles) 277 } else { 278 err = h.Client.Template(chartDir, releaseName, ns, outputDir, false, values, valueStrings, valueFiles) 279 } 280 if err != nil { 281 return err 282 } 283 284 // Skip the chart when no resources are generated by the template 285 if empty, err := util.IsEmpty(outputDir); empty || err != nil { 286 return nil 287 } 288 289 metadata, versionText, err := h.getChart(chartDir, version) 290 if err != nil { 291 return err 292 } 293 294 helmHooks, err := h.addLabelsToFiles(chart, releaseName, versionText, metadata, ns) 295 if err != nil { 296 return err 297 } 298 helmCrdPhase := "crd-install" 299 helmPrePhase := "pre-install" 300 helmPostPhase := "post-install" 301 if !create { 302 helmPrePhase = "pre-upgrade" 303 helmPostPhase = "post-upgrade" 304 305 // Hack helm hooks 306 if len(MatchingHooks(helmHooks, "post-install", "")) > 0 && 307 len(MatchingHooks(helmHooks, helmPostPhase, "")) == 0 { 308 helmPostPhase = "post-install" 309 if len(MatchingHooks(helmHooks, "pre-install", "")) > 0 && 310 len(MatchingHooks(helmHooks, helmPrePhase, "")) == 0 { 311 helmPrePhase = "pre-install" 312 } 313 } 314 } 315 316 err = h.runHooks(helmHooks, helmCrdPhase, ns, chart, releaseName, wait, create, force) 317 if err != nil { 318 return err 319 } 320 321 err = h.runHooks(helmHooks, helmPrePhase, ns, chart, releaseName, wait, create, force) 322 if err != nil { 323 return err 324 } 325 326 err = h.kubectlApply(ns, releaseName, wait, create, force, outputDir) 327 if err != nil { 328 err2 := h.deleteHooks(helmHooks, helmPrePhase, hookFailed, ns) 329 return errorutil.CombineErrors(err, err2) 330 } 331 err = h.deleteHooks(helmHooks, helmPrePhase, hookSucceeded, ns) 332 if err != nil { 333 log.Logger().Warnf("Failed to delete the %s hook, due to: %s", helmPrePhase, err) 334 } 335 336 err = h.runHooks(helmHooks, helmPostPhase, ns, chart, releaseName, wait, create, force) 337 if err != nil { 338 err2 := h.deleteHooks(helmHooks, helmPostPhase, hookFailed, ns) 339 return errorutil.CombineErrors(err, err2) 340 } 341 342 err = h.deleteHooks(helmHooks, helmPostPhase, hookSucceeded, ns) 343 err2 := h.deleteOldResources(ns, releaseName, versionText, wait) 344 345 return errorutil.CombineErrors(err, err2) 346 } 347 348 // InstallChart installs a helm chart according with the given flags 349 func (h *HelmTemplate) InstallChart(chart string, releaseName string, ns string, version string, timeout int, 350 values []string, valueStrings []string, valueFiles []string, repo string, username string, password string) error { 351 return h.auxInstallChart(false, true, true, true, chart, releaseName, ns, 352 version, timeout, values, valueStrings, valueFiles, repo, username, password) 353 } 354 355 // UpgradeChart upgrades a helm chart according with given helm flags 356 func (h *HelmTemplate) UpgradeChart(chart string, releaseName string, ns string, version string, install bool, timeout int, force bool, wait bool, values []string, valueStrings []string, valueFiles []string, repo string, username string, password string) error { 357 return h.auxInstallChart(false, false, force, wait, chart, releaseName, ns, 358 version, timeout, values, valueStrings, valueFiles, repo, username, password) 359 } 360 361 // MultiInstallChart installs a helm chart according with the given flags 362 func (h *HelmTemplate) InstallMultiChart(chart string, releaseName string, ns string, version string, timeout int, 363 values []string, valueStrings []string, valueFiles []string, repo string, username string, password string) error { 364 return h.auxInstallChart(true, true, true, true, chart, releaseName, ns, 365 version, timeout, values, valueStrings, valueFiles, repo, username, password) 366 } 367 368 // MultiUpgradeChart upgrades a helm chart according with given helm flags 369 func (h *HelmTemplate) UpgradeMultiChart(chart string, releaseName string, ns string, version string, install bool, timeout int, force bool, wait bool, values []string, valueStrings []string, valueFiles []string, repo string, username string, password string) error { 370 return h.auxInstallChart(true, false, force, wait, chart, releaseName, ns, 371 version, timeout, values, valueStrings, valueFiles, repo, username, password) 372 } 373 374 // FetchChart fetches a Helm Chart 375 func (h *HelmTemplate) FetchChart(chart string, version string, untar bool, untardir string, repo string, 376 username string, password string) error { 377 _, err := h.fetchChart(chart, version, untardir, repo, username, password) 378 return err 379 } 380 381 func (h *HelmTemplate) DecryptSecrets(location string) error { 382 return h.Client.DecryptSecrets(location) 383 } 384 385 func (h *HelmTemplate) kubectlApply(ns string, releaseName string, wait bool, create bool, force bool, dir string) error { 386 namespacesDir := filepath.Join(dir, "namespaces") 387 if _, err := os.Stat(namespacesDir); !os.IsNotExist(err) { 388 389 fileInfo, err := ioutil.ReadDir(namespacesDir) 390 if err != nil { 391 return errors.Wrapf(err, "unable to locate subdirs in %s", namespacesDir) 392 } 393 394 for _, path := range fileInfo { 395 namespace := filepath.Base(path.Name()) 396 fullPath := filepath.Join(namespacesDir, path.Name()) 397 398 log.Logger().Debugf("Applying generated chart %q YAML via kubectl in dir: %s to namespace %s", releaseName, fullPath, namespace) 399 400 command := "apply" 401 if create { 402 command = "create" 403 } 404 args := []string{command, "--recursive", "-f", fullPath, "-l", LabelReleaseName + "=" + releaseName} 405 applyNs := namespace 406 if applyNs == "" { 407 applyNs = ns 408 } 409 if applyNs != "" { 410 args = append(args, "--namespace", applyNs) 411 } 412 if wait && !create { 413 args = append(args, "--wait") 414 } 415 if !h.KubectlValidate { 416 args = append(args, "--validate=false") 417 } 418 err = h.runKubectl(args...) 419 if err != nil { 420 return err 421 } 422 log.Logger().Info("") 423 } 424 return err 425 } 426 427 log.Logger().Debugf("Applying generated chart %q YAML via kubectl in dir: %s to namespace %s", releaseName, dir, ns) 428 command := "apply" 429 if create { 430 command = "create" 431 } 432 args := []string{command, "--recursive", "-f", dir, "-l", LabelReleaseName + "=" + releaseName} 433 if ns != "" { 434 args = append(args, "--namespace", ns) 435 } 436 if wait && !create { 437 args = append(args, "--wait") 438 } 439 if force { 440 args = append(args, "--force") 441 } 442 if !h.KubectlValidate { 443 args = append(args, "--validate=false") 444 } 445 err := h.runKubectl(args...) 446 if err != nil { 447 return err 448 } 449 450 return nil 451 452 } 453 454 func (h *HelmTemplate) kubectlApplyFile(ns string, helmHook string, wait bool, create bool, force bool, file string) error { 455 log.Logger().Debugf("Applying Helm hook %s YAML via kubectl in file: %s", helmHook, file) 456 457 command := "apply" 458 if create { 459 command = "create" 460 } 461 args := []string{command, "-f", file} 462 if ns != "" { 463 args = append(args, "--namespace", ns) 464 } 465 if wait && !create { 466 args = append(args, "--wait") 467 } 468 if force && !create { 469 args = append(args, "--force") 470 } 471 if !h.KubectlValidate { 472 args = append(args, "--validate=false") 473 } 474 err := h.runKubectl(args...) 475 return err 476 } 477 478 func (h *HelmTemplate) kubectlDeleteFile(ns string, file string) error { 479 log.Logger().Debugf("Deleting helm hook sources from file: %s", file) 480 return h.runKubectl("delete", "-f", file, "--namespace", ns, "--wait") 481 } 482 483 func (h *HelmTemplate) deleteOldResources(ns string, releaseName string, versionText string, wait bool) error { 484 selector := LabelReleaseName + "=" + releaseName + "," + 485 LabelReleaseChartVersion + "," + 486 LabelReleaseChartVersion + "!=" + versionText 487 err := h.deleteNamespacedResourcesBySelector(ns, selector, wait, "older releases") 488 if err != nil { 489 return err 490 } 491 return h.deleteClusterResourcesBySelector(ns, selector, wait, "older releases") 492 } 493 494 func (h *HelmTemplate) deleteNamespacedResourcesBySelector(ns string, selector string, wait bool, message string) error { 495 kinds := []string{"job", "deployment", "pvc", "configmap", "release", "sa", "role", "rolebinding", "secret"} 496 errList := []error{} 497 log.Logger().Debugf("Removing Kubernetes resources from %s using selector: %s from %s", message, util.ColorInfo(selector), strings.Join(kinds, " ")) 498 errs := h.deleteResourcesBySelector(ns, kinds, selector, wait) 499 errList = append(errList, errs...) 500 return errorutil.CombineErrors(errList...) 501 } 502 503 func (h *HelmTemplate) deleteClusterResourcesBySelector(ns string, selector string, wait bool, message string) error { 504 clusterKinds := []string{"all", "clusterrole", "clusterrolebinding"} 505 errList := []error{} 506 hasPermissions, errs := kube.CanI(h.KubeClient, kube.Delete, kube.All) 507 errList = append(errList, errs...) 508 if hasPermissions { 509 selector += "," + LabelNamespace + "=" + ns 510 log.Logger().Debugf("Removing Kubernetes resources from %s using selector: %s from %s", message, util.ColorInfo(selector), strings.Join(clusterKinds, " ")) 511 errs = h.deleteResourcesBySelector("", clusterKinds, selector, wait) 512 errList = append(errList, errs...) 513 } 514 return errorutil.CombineErrors(errList...) 515 } 516 517 func (h *HelmTemplate) deleteResourcesBySelector(ns string, kinds []string, selector string, wait bool) []error { 518 errList := []error{} 519 for _, kind := range kinds { 520 args := []string{"delete", kind, "--ignore-not-found", "-l", selector} 521 if ns != "" { 522 args = append(args, "--namespace", ns) 523 } 524 if wait { 525 args = append(args, "--wait") 526 } 527 output, err := h.runKubectlWithOutput(args...) 528 if err != nil { 529 errList = append(errList, err) 530 } else { 531 output = strings.TrimSpace(output) 532 if output != "No resources found" { 533 log.Logger().Info(output) 534 } 535 } 536 } 537 return errList 538 } 539 540 // isClusterKind returns true if the kind or resource name is a cluster wide resource 541 func isClusterKind(kind string) bool { 542 lower := strings.ToLower(kind) 543 return strings.HasPrefix(lower, "cluster") || strings.HasPrefix(lower, "namespace") 544 } 545 546 // DeleteRelease removes the given release 547 func (h *HelmTemplate) DeleteRelease(ns string, releaseName string, purge bool) error { 548 if ns == "" { 549 ns = h.Namespace 550 } 551 selector := LabelReleaseName + "=" + releaseName 552 err := h.deleteNamespacedResourcesBySelector(ns, selector, true, fmt.Sprintf("release %s", releaseName)) 553 if err != nil { 554 return err 555 } 556 return h.deleteClusterResourcesBySelector(ns, selector, true, fmt.Sprintf("release %s", releaseName)) 557 } 558 559 // StatusRelease returns the output of the helm status command for a given release 560 func (h *HelmTemplate) StatusRelease(ns string, releaseName string) error { 561 releases, _, err := h.ListReleases(ns) 562 if err != nil { 563 return errors.Wrap(err, "listing current chart releases") 564 } 565 if _, ok := releases[releaseName]; ok { 566 return nil 567 } 568 return fmt.Errorf("chart release %q not found", releaseName) 569 } 570 571 // StatusReleaseWithOutput returns the output of the helm status command for a given release 572 func (h *HelmTemplate) StatusReleaseWithOutput(ns string, releaseName string, outputFormat string) (string, error) { 573 return h.Client.StatusReleaseWithOutput(ns, releaseName, outputFormat) 574 } 575 576 func (h *HelmTemplate) getDirectories(releaseName string) (string, string, string, error) { 577 if releaseName == "" { 578 return "", "", "", fmt.Errorf("No release name specified!") 579 } 580 if h.WorkDir == "" { 581 var err error 582 h.WorkDir, err = ioutil.TempDir("", "helm-template-workdir-") 583 if err != nil { 584 return "", "", "", errors.Wrap(err, "Failed to create temporary directory for helm template workdir") 585 } 586 } 587 workDir := h.WorkDir 588 outDir := filepath.Join(workDir, releaseName, "output") 589 helmHookDir := filepath.Join(workDir, releaseName, "helmHooks") 590 chartsDir := filepath.Join(workDir, releaseName, "chartFiles") 591 592 dirs := []string{outDir, helmHookDir, chartsDir} 593 for _, d := range dirs { 594 err := os.MkdirAll(d, util.DefaultWritePermissions) 595 if err != nil { 596 return "", "", "", err 597 } 598 } 599 return outDir, helmHookDir, chartsDir, nil 600 } 601 602 // clearOutputDir removes all files in the helm output dir 603 func (h *HelmTemplate) clearOutputDir(releaseName string) error { 604 dir, helmDir, chartsDir, err := h.getDirectories(releaseName) 605 if err != nil { 606 return err 607 } 608 return util.RecreateDirs(dir, helmDir, chartsDir) 609 } 610 611 func (h *HelmTemplate) fetchChart(chart string, version string, dir string, repo string, username string, 612 password string) (string, error) { 613 exists, err := util.FileExists(chart) 614 if err != nil { 615 return "", err 616 } 617 if exists { 618 log.Logger().Infof("Chart dir already exists: %s", dir) 619 return chart, nil 620 } 621 if dir == "" { 622 return "", fmt.Errorf("must specify dir for chart %s", chart) 623 } 624 args := []string{ 625 "fetch", "-d", dir, "--untar", chart, 626 } 627 if repo != "" { 628 args = append(args, "--repo", repo) 629 } 630 if version != "" { 631 args = append(args, "--version", version) 632 } 633 if username != "" { 634 args = append(args, "--username", username) 635 } 636 if password != "" { 637 args = append(args, "--password", password) 638 } 639 err = h.Client.runHelm(args...) 640 if err != nil { 641 return "", err 642 } 643 answer := dir 644 files, err := ioutil.ReadDir(dir) 645 if err != nil { 646 return "", err 647 } 648 649 for _, f := range files { 650 if f.IsDir() { 651 answer = filepath.Join(dir, f.Name()) 652 break 653 } 654 } 655 log.Logger().Debugf("Fetched chart %s to dir %s", chart, answer) 656 return answer, nil 657 } 658 659 func (h *HelmTemplate) addLabelsToFiles(chart string, releaseName string, version string, metadata *chart.Metadata, ns string) ([]*HelmHook, error) { 660 dir, helmHookDir, _, err := h.getDirectories(releaseName) 661 if err != nil { 662 return nil, err 663 } 664 return addLabelsToChartYaml(dir, helmHookDir, chart, releaseName, version, metadata, ns) 665 } 666 667 func splitObjectsInFiles(inputFile string, baseDir string, relativePath, defaultNamespace string) ([]string, error) { 668 result := make([]string, 0) 669 f, err := os.Open(inputFile) 670 if err != nil { 671 return result, errors.Wrapf(err, "opening inputFile %q", inputFile) 672 } 673 defer f.Close() 674 scanner := bufio.NewScanner(f) 675 var buf bytes.Buffer 676 fileName := filepath.Base(inputFile) 677 count := 0 678 for scanner.Scan() { 679 line := scanner.Text() 680 if line == resourcesSeparator { 681 // ensure that we actually have YAML in the buffer 682 data := buf.Bytes() 683 if isWhitespaceOrComments(data) { 684 buf.Reset() 685 continue 686 } 687 688 m := yaml.MapSlice{} 689 err = yaml.Unmarshal(data, &m) 690 691 namespace := getYamlValueString(&m, "metadata", "namespace") 692 if namespace == "" { 693 namespace = defaultNamespace 694 } 695 696 if err != nil { 697 return make([]string, 0), errors.Wrapf(err, "Failed to parse the following YAML from inputFile '%s':\n%s", inputFile, buf.String()) 698 } 699 if len(m) == 0 { 700 buf.Reset() 701 continue 702 } 703 704 partFile, err := writeObjectInFile(&buf, baseDir, relativePath, namespace, fileName, count) 705 if err != nil { 706 return result, errors.Wrapf(err, "saving object") 707 } 708 result = append(result, partFile) 709 buf.Reset() 710 count += count + 1 711 } else { 712 _, err := buf.WriteString(line) 713 if err != nil { 714 return result, errors.Wrapf(err, "writing line from inputFile %q into a buffer", inputFile) 715 } 716 _, err = buf.WriteString("\n") 717 if err != nil { 718 return result, errors.Wrapf(err, "writing a new line in the buffer") 719 } 720 } 721 } 722 if buf.Len() > 0 && !isWhitespaceOrComments(buf.Bytes()) { 723 data := buf.Bytes() 724 725 m := yaml.MapSlice{} 726 err = yaml.Unmarshal(data, &m) 727 728 namespace := getYamlValueString(&m, "metadata", "namespace") 729 if namespace == "" { 730 namespace = defaultNamespace 731 } 732 733 partFile, err := writeObjectInFile(&buf, baseDir, relativePath, namespace, fileName, count) 734 if err != nil { 735 return result, errors.Wrapf(err, "saving object") 736 } 737 result = append(result, partFile) 738 } 739 740 return result, nil 741 } 742 743 // isWhitespaceOrComments returns true if the data is empty, whitespace or comments only 744 func isWhitespaceOrComments(data []byte) bool { 745 if len(data) == 0 { 746 return true 747 } 748 lines := strings.Split(string(data), "\n") 749 for _, line := range lines { 750 t := strings.TrimSpace(line) 751 if t != "" && !strings.HasPrefix(t, "#") { 752 return false 753 } 754 } 755 return true 756 } 757 758 func writeObjectInFile(buf io.WriterTo, baseDir string, relativePath, namespace string, fileName string, count int) (string, error) { 759 relativeDir := filepath.Dir(relativePath) 760 761 const filePrefix = "part" 762 partFile := fmt.Sprintf("%s%d-%s", filePrefix, count, fileName) 763 absFile := filepath.Join(baseDir, "namespaces", namespace, relativeDir, partFile) 764 765 absFileDir := filepath.Dir(absFile) 766 767 log.Logger().Debugf("creating file: %s", absFile) 768 769 err := os.MkdirAll(absFileDir, os.ModePerm) 770 if err != nil { 771 return "", errors.Wrapf(err, "creating directory %q", absFileDir) 772 } 773 file, err := os.Create(absFile) 774 if err != nil { 775 return "", errors.Wrapf(err, "creating file %q", absFile) 776 } 777 778 log.Logger().Debugf("writing data to %s", absFile) 779 780 defer file.Close() 781 _, err = buf.WriteTo(file) 782 if err != nil { 783 return "", errors.Wrapf(err, "writing object to file %q", absFile) 784 } 785 return absFile, nil 786 } 787 788 func addLabelsToChartYaml(basedir string, hooksDir string, chart string, releaseName string, version string, metadata *chart.Metadata, ns string) ([]*HelmHook, error) { 789 helmHooks := []*HelmHook{} 790 791 log.Logger().Debugf("Searching for yaml files from basedir %s", basedir) 792 793 err := filepath.Walk(basedir, func(path string, f os.FileInfo, err error) error { 794 ext := filepath.Ext(path) 795 if ext == ".yaml" { 796 file := path 797 798 relativePath, err := filepath.Rel(basedir, file) 799 if err != nil { 800 return errors.Wrapf(err, "unable to determine relative path %q", file) 801 } 802 803 partFiles, err := splitObjectsInFiles(file, basedir, relativePath, ns) 804 if err != nil { 805 return errors.Wrapf(err, "splitting objects from file %q", file) 806 } 807 808 log.Logger().Debugf("part files list: %v", partFiles) 809 810 for _, partFile := range partFiles { 811 log.Logger().Debugf("processing part file: %s", partFile) 812 data, err := ioutil.ReadFile(partFile) 813 if err != nil { 814 return errors.Wrapf(err, "Failed to load partFile %s", partFile) 815 } 816 m := yaml.MapSlice{} 817 err = yaml.Unmarshal(data, &m) 818 if err != nil { 819 return errors.Wrapf(err, "Failed to parse YAML of partFile %s", partFile) 820 } 821 kind := getYamlValueString(&m, "kind") 822 helmHookType := getYamlValueString(&m, "metadata", "annotations", "helm.sh/hook") 823 err = processChartResource(partFile, data, kind, ns, releaseName, &m, metadata, version, chart, helmHookType != "") 824 if err != nil { 825 return errors.Wrap(err, fmt.Sprintf("when processing chart resource '%s'", partFile)) 826 } 827 if helmHookType != "" { 828 helmHook, err := getHelmHookFromFile(basedir, path, hooksDir, helmHookType, kind, &m, partFile) 829 if err != nil { 830 return errors.Wrap(err, fmt.Sprintf("when getting helm hook from part file '%s'", partFile)) 831 } 832 helmHooks = append(helmHooks, helmHook) 833 } 834 } 835 } 836 return nil 837 }) 838 839 return helmHooks, err 840 } 841 842 func getHelmHookFromFile(basedir string, path string, hooksDir string, helmHook string, kind string, m *yaml.MapSlice, partFile string) (*HelmHook, error) { 843 // lets move any helm hooks to the new partFile 844 relPath, err := filepath.Rel(basedir, path) 845 if err != nil { 846 return &HelmHook{}, err 847 } 848 if relPath == "" { 849 return &HelmHook{}, fmt.Errorf("Failed to find relative path of basedir %s and path %s", basedir, partFile) 850 } 851 852 // add the hook type into the directory structure 853 newPath := filepath.Join(hooksDir, relPath) 854 newDir, _ := filepath.Split(newPath) 855 err = os.MkdirAll(newDir, util.DefaultWritePermissions) 856 if err != nil { 857 return &HelmHook{}, errors.Wrap(err, fmt.Sprintf("when creating '%s'", newDir)) 858 } 859 860 // copy the hook part file to the hooks path 861 _, hookFileName := filepath.Split(partFile) 862 hookFile := filepath.Join(newDir, hookFileName) 863 err = os.Rename(partFile, hookFile) 864 if err != nil { 865 return &HelmHook{}, errors.Wrap(err, fmt.Sprintf("when copying from '%s' to '%s'", partFile, hookFile)) 866 } 867 868 name := getYamlValueString(m, "metadata", "name") 869 helmDeletePolicy := getYamlValueString(m, "metadata", "annotations", "helm.sh/hook-delete-policy") 870 log.Logger().Debugf("processed hook %s (%s | %s) to file: %s", kind, helmHook, helmDeletePolicy, hookFile) 871 return NewHelmHook(kind, name, hookFile, helmHook, helmDeletePolicy), nil 872 } 873 874 func processChartResource(partFile string, data []byte, kind string, ns string, releaseName string, m *yaml.MapSlice, metadata *chart.Metadata, version string, chart string, hook bool) error { 875 err := setYamlValue(m, releaseName, "metadata", "labels", LabelReleaseName) 876 if err != nil { 877 return errors.Wrapf(err, "Failed to modify YAML of partFile %s", partFile) 878 } 879 if !isClusterKind(kind) { 880 err = setYamlValue(m, ns, "metadata", "labels", LabelNamespace) 881 if err != nil { 882 return errors.Wrapf(err, "Failed to modify YAML of partFile %s", partFile) 883 } 884 } 885 if hook { 886 err = setYamlValue(m, version, "metadata", "labels", LabelReleaseHookChartVersion) 887 } else { 888 err = setYamlValue(m, version, "metadata", "labels", LabelReleaseChartVersion) 889 } 890 if err != nil { 891 return errors.Wrapf(err, "Failed to modify YAML of partFile %s", partFile) 892 } 893 chartName := "" 894 895 if metadata != nil { 896 chartName = metadata.GetName() 897 appVersion := metadata.GetAppVersion() 898 if appVersion != "" { 899 err = setYamlValue(m, appVersion, "metadata", "annotations", AnnotationAppVersion) 900 if err != nil { 901 return errors.Wrapf(err, "Failed to modify YAML of partFile %s", partFile) 902 } 903 } 904 } 905 if chartName == "" { 906 chartName = chart 907 } 908 err = setYamlValue(m, chartName, "metadata", "annotations", AnnotationChartName) 909 if err != nil { 910 return errors.Wrapf(err, "Failed to modify YAML of partFile %s", partFile) 911 } 912 913 data, err = yaml.Marshal(m) 914 if err != nil { 915 return errors.Wrapf(err, "Failed to marshal YAML of partFile %s", partFile) 916 } 917 err = ioutil.WriteFile(partFile, data, util.DefaultWritePermissions) 918 if err != nil { 919 return errors.Wrapf(err, "Failed to write YAML partFile %s", partFile) 920 } 921 log.Logger().Debugf("processed resource %s in part file: %s", kind, partFile) 922 return nil 923 } 924 925 func getYamlValueString(mapSlice *yaml.MapSlice, keys ...string) string { 926 value := getYamlValue(mapSlice, keys...) 927 answer, ok := value.(string) 928 if ok { 929 930 return answer 931 } 932 return "" 933 } 934 935 func getYamlValue(mapSlice *yaml.MapSlice, keys ...string) interface{} { 936 if mapSlice == nil { 937 return nil 938 } 939 if mapSlice == nil { 940 return fmt.Errorf("No map input!") 941 } 942 m := mapSlice 943 lastIdx := len(keys) - 1 944 for idx, k := range keys { 945 last := idx >= lastIdx 946 found := false 947 for _, mi := range *m { 948 if mi.Key == k { 949 found = true 950 if last { 951 return mi.Value 952 } else { 953 value := mi.Value 954 if value == nil { 955 return nil 956 } else { 957 v, ok := value.(yaml.MapSlice) 958 if ok { 959 m = &v 960 } else { 961 v2, ok := value.(*yaml.MapSlice) 962 if ok { 963 m = v2 964 } else { 965 return nil 966 } 967 } 968 } 969 } 970 } 971 } 972 if !found { 973 return nil 974 } 975 } 976 return nil 977 978 } 979 980 // setYamlValue navigates through the YAML object structure lazily creating or inserting new values 981 func setYamlValue(mapSlice *yaml.MapSlice, value string, keys ...string) error { 982 if mapSlice == nil { 983 return fmt.Errorf("No map input!") 984 } 985 m := mapSlice 986 lastIdx := len(keys) - 1 987 for idx, k := range keys { 988 last := idx >= lastIdx 989 found := false 990 for i, mi := range *m { 991 if mi.Key == k { 992 found = true 993 if last { 994 (*m)[i].Value = value 995 } else if i < len(*m) { 996 value := (*m)[i].Value 997 if value == nil { 998 v := &yaml.MapSlice{} 999 (*m)[i].Value = v 1000 m = v 1001 } else { 1002 v, ok := value.(yaml.MapSlice) 1003 if ok { 1004 m2 := &yaml.MapSlice{} 1005 *m2 = append(*m2, v...) 1006 (*m)[i].Value = m2 1007 m = m2 1008 } else { 1009 v2, ok := value.(*yaml.MapSlice) 1010 if ok { 1011 m2 := &yaml.MapSlice{} 1012 *m2 = append(*m2, *v2...) 1013 (*m)[i].Value = m2 1014 m = m2 1015 } else { 1016 return fmt.Errorf("Could not convert key %s value %#v to a yaml.MapSlice", k, value) 1017 } 1018 } 1019 } 1020 } 1021 } 1022 } 1023 if !found { 1024 if last { 1025 *m = append(*m, yaml.MapItem{ 1026 Key: k, 1027 Value: value, 1028 }) 1029 } else { 1030 m2 := &yaml.MapSlice{} 1031 *m = append(*m, yaml.MapItem{ 1032 Key: k, 1033 Value: m2, 1034 }) 1035 m = m2 1036 } 1037 } 1038 } 1039 return nil 1040 } 1041 1042 func (h *HelmTemplate) runKubectl(args ...string) error { 1043 _, err := h.runKubectlWithOutput(args...) 1044 return err 1045 } 1046 1047 func (h *HelmTemplate) runKubectlWithOutput(args ...string) (string, error) { 1048 h.Runner.SetDir(h.CWD) 1049 h.Runner.SetName(h.Binary) 1050 h.Runner.SetArgs(args) 1051 return h.Runner.RunWithoutRetry() 1052 // fmt.Printf("runKubectl: %s\n", strings.Join(args, " ")) 1053 // if args[0] == "create" || args[0] == "apply" { 1054 // args = append([]string{args[0], "--dry-run"}, args[1:len(args)]...) 1055 // } else if args[0] == "delete" { 1056 // args = append([]string{"get"}, args[1:len(args)]...) 1057 // } else { 1058 // return "", fmt.Errorf("unknown command 'kubectl %s'", args[0]) 1059 // } 1060 // output, err := h.Runner.RunWithoutRetry() 1061 // fmt.Printf("runKubectl: \n %s\n", strings.Join(strings.Split(output, "\n"), " \n")) 1062 // log.Logger().Debugf(output) 1063 // return output, err 1064 } 1065 1066 // getChart returns the chart metadata for the given dir 1067 func (h *HelmTemplate) getChart(chartDir string, version string) (*chart.Metadata, string, error) { 1068 file := filepath.Join(chartDir, ChartFileName) 1069 if !filepath.IsAbs(chartDir) { 1070 file = filepath.Join(h.Runner.CurrentDir(), file) 1071 } 1072 exists, err := util.FileExists(file) 1073 if err != nil { 1074 return nil, version, err 1075 } 1076 if !exists { 1077 return nil, version, fmt.Errorf("no file %s found!", file) 1078 } 1079 metadata, err := chartutil.LoadChartfile(file) 1080 if version == "" && metadata != nil { 1081 version = metadata.GetVersion() 1082 } 1083 return metadata, version, err 1084 } 1085 1086 func (h *HelmTemplate) runHooks(hooks []*HelmHook, hookPhase string, ns string, chart string, releaseName string, wait bool, create bool, force bool) error { 1087 log.Logger().Debugf("running hook phase %s in namespace %s", hookPhase, ns) 1088 matchingHooks := MatchingHooks(hooks, hookPhase, "") 1089 for _, hook := range matchingHooks { 1090 log.Logger().Debugf("applying hook %s %s from file: %s", hook.Kind, hook.Name, hook.File) 1091 if util.StringArrayIndex(hook.HookDeletePolicies, beforeHookCreation) >= 0 { 1092 h.kubectlDeleteFile(ns, hook.File) 1093 } 1094 err := h.kubectlApplyFile(ns, hookPhase, wait, create, force, hook.File) 1095 if err != nil { 1096 return err 1097 } 1098 } 1099 return nil 1100 } 1101 1102 func (h *HelmTemplate) deleteHooks(hooks []*HelmHook, hookPhase string, hookDeletePolicy string, ns string) error { 1103 log.Logger().Debugf("deleting hook phase %s (%s) in namespace %s", hookPhase, hookDeletePolicy, ns) 1104 flag := os.Getenv("JX_DISABLE_DELETE_HELM_HOOKS") 1105 matchingHooks := MatchingHooks(hooks, hookPhase, hookDeletePolicy) 1106 for _, hook := range matchingHooks { 1107 kind := hook.Kind 1108 name := hook.Name 1109 if kind == "Job" && name != "" { 1110 log.Logger().Debugf("Waiting for helm %s hook Job %s to complete before removing it", hookPhase, name) 1111 err := kube.WaitForJobToComplete(h.KubeClient, ns, name, time.Minute*30, false) 1112 if err != nil { 1113 log.Logger().Warnf("Job %s has not yet terminated for helm hook phase %s due to: %s so removing it anyway", name, hookPhase, err) 1114 } 1115 } else { 1116 log.Logger().Warnf("Could not wait for hook resource to complete as it is kind %s and name %s for phase %s", kind, name, hookPhase) 1117 } 1118 if flag == "true" { 1119 log.Logger().Infof("Not deleting the Job %s as we have the $JX_DISABLE_DELETE_HELM_HOOKS enabled", name) 1120 continue 1121 } 1122 log.Logger().Debugf("deleting hook %s %s from file: %s", hook.Kind, hook.Name, hook.File) 1123 err := h.kubectlDeleteFile(ns, hook.File) 1124 if err != nil { 1125 return err 1126 } 1127 } 1128 return nil 1129 } 1130 1131 // NewHelmHook returns a newly created HelmHook 1132 func NewHelmHook(kind string, name string, file string, hook string, hookDeletePolicy string) *HelmHook { 1133 if hookDeletePolicy == "" { 1134 hookDeletePolicy = beforeHookCreation 1135 } 1136 return &HelmHook{ 1137 Kind: kind, 1138 Name: name, 1139 File: file, 1140 Hooks: strings.Split(hook, ","), 1141 HookDeletePolicies: strings.Split(hookDeletePolicy, ","), 1142 } 1143 } 1144 1145 // MatchingHooks returns the matching files which have the given hook name and if hookPolicy is not blank the hook policy too 1146 func MatchingHooks(hooks []*HelmHook, hook string, hookDeletePolicy string) []*HelmHook { 1147 answer := []*HelmHook{} 1148 for _, h := range hooks { 1149 if util.StringArrayIndex(h.Hooks, hook) >= 0 && 1150 (hookDeletePolicy == "" || util.StringArrayIndex(h.HookDeletePolicies, hookDeletePolicy) >= 0) { 1151 answer = append(answer, h) 1152 } 1153 } 1154 return answer 1155 }