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  }