github.com/jenkins-x/jx/v2@v2.1.155/pkg/environments/gitops.go (about)

     1  package environments
     2  
     3  import (
     4  	"fmt"
     5  	"io/ioutil"
     6  	"net/url"
     7  	"os"
     8  	"path/filepath"
     9  	"strings"
    10  
    11  	jenkinsio "github.com/jenkins-x/jx-api/pkg/apis/jenkins.io"
    12  
    13  	"github.com/ghodss/yaml"
    14  
    15  	"github.com/pkg/errors"
    16  
    17  	"k8s.io/helm/pkg/proto/hapi/chart"
    18  
    19  	jenkinsv1 "github.com/jenkins-x/jx-api/pkg/apis/jenkins.io/v1"
    20  	"github.com/jenkins-x/jx-logging/pkg/log"
    21  	"github.com/jenkins-x/jx/v2/pkg/gits"
    22  	"github.com/jenkins-x/jx/v2/pkg/helm"
    23  	"github.com/jenkins-x/jx/v2/pkg/util"
    24  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    25  	helmchart "k8s.io/helm/pkg/proto/hapi/chart"
    26  )
    27  
    28  //ValuesFiles is a wrapper for a slice of values files to allow them to be passed around as a pointer
    29  type ValuesFiles struct {
    30  	Items []string
    31  }
    32  
    33  // ModifyChartFn callback for modifying a chart, requirements, the chart metadata,
    34  // the values.yaml and all files in templates are unmarshaled, and the root dir for the chart is passed
    35  type ModifyChartFn func(requirements *helm.Requirements, metadata *chart.Metadata, existingValues map[string]interface{},
    36  	templates map[string]string, dir string, pullRequestDetails *gits.PullRequestDetails) error
    37  
    38  // EnvironmentPullRequestOptions are options for creating a pull request against an environment.
    39  // The provide a Gitter client for performing git operations, a GitProvider client for talking to the git provider,
    40  // a callback ModifyChartFn which is where the changes you want to make are defined,
    41  type EnvironmentPullRequestOptions struct {
    42  	Gitter        gits.Gitter
    43  	GitProvider   gits.GitProvider
    44  	ModifyChartFn ModifyChartFn
    45  	Labels        []string
    46  }
    47  
    48  // Create a pull request against the environment repository for env.
    49  // The EnvironmentPullRequestOptions are used to provide a Gitter client for performing git operations,
    50  // a GitProvider client for talking to the git provider,
    51  // a callback ModifyChartFn which is where the changes you want to make are defined.
    52  // The branchNameText defines the branch name used, the title is used for both the commit and the pull request title,
    53  // the message as the body for both the commit and the pull request,
    54  // and the pullRequestInfo for any existing PR that exists to modify the environment that we want to merge these
    55  // changes into.
    56  func (o *EnvironmentPullRequestOptions) Create(env *jenkinsv1.Environment, prDir string,
    57  	pullRequestDetails *gits.PullRequestDetails, filter *gits.PullRequestFilter, chartName string, autoMerge bool) (*gits.PullRequestInfo, error) {
    58  	if prDir == "" {
    59  		tempDir, err := ioutil.TempDir("", "create-pr")
    60  		if err != nil {
    61  			return nil, err
    62  		}
    63  		prDir = tempDir
    64  		defer os.RemoveAll(tempDir)
    65  	}
    66  
    67  	dir, base, upstreamRepo, forkURL, err := gits.ForkAndPullRepo(env.Spec.Source.URL, prDir, env.Spec.Source.Ref, pullRequestDetails.BranchName, o.GitProvider, o.Gitter, "")
    68  
    69  	if err != nil {
    70  		return nil, errors.Wrapf(err, "pulling environment repo %s into %s", env.Spec.Source.URL,
    71  			prDir)
    72  	}
    73  
    74  	err = ModifyChartFiles(dir, pullRequestDetails, o.ModifyChartFn, chartName)
    75  	if err != nil {
    76  		return nil, err
    77  	}
    78  	labels := make([]string, 0)
    79  	labels = append(labels, pullRequestDetails.Labels...)
    80  	labels = append(labels, o.Labels...)
    81  	if autoMerge {
    82  		labels = append(labels, gits.LabelUpdatebot)
    83  	}
    84  	pullRequestDetails.Labels = labels
    85  	prInfo, err := gits.PushRepoAndCreatePullRequest(dir, upstreamRepo, forkURL, base, pullRequestDetails, filter, true, pullRequestDetails.Message, true, false, o.Gitter, o.GitProvider)
    86  	if err != nil {
    87  		return nil, err
    88  	}
    89  	return prInfo, nil
    90  }
    91  
    92  // ModifyChartFiles modifies the chart files in the given directory using the given modify function
    93  func ModifyChartFiles(dir string, details *gits.PullRequestDetails, modifyFn ModifyChartFn, chartName string) error {
    94  	requirementsFile, err := helm.FindRequirementsFileName(dir)
    95  	if err != nil {
    96  		return err
    97  	}
    98  	requirements, err := helm.LoadRequirementsFile(requirementsFile)
    99  	if err != nil {
   100  		return err
   101  	}
   102  
   103  	chartFile, err := helm.FindChartFileName(dir)
   104  	if err != nil {
   105  		return err
   106  	}
   107  
   108  	chart, err := helm.LoadChartFile(chartFile)
   109  	if err != nil {
   110  		return err
   111  	}
   112  
   113  	valuesFile, err := helm.FindValuesFileNameForChart(dir, chartName)
   114  	if err != nil {
   115  		return err
   116  	}
   117  
   118  	values, err := helm.LoadValuesFile(valuesFile)
   119  	if err != nil {
   120  		return err
   121  	}
   122  
   123  	templatesDir, err := helm.FindTemplatesDirName(dir)
   124  	if err != nil {
   125  		return err
   126  	}
   127  	templates, err := helm.LoadTemplatesDir(templatesDir)
   128  	if err != nil {
   129  		return err
   130  	}
   131  
   132  	// lets pass in the folder containing the `Chart.yaml` which is the `env` dir in GitOps management
   133  	chartDir, _ := filepath.Split(chartFile)
   134  
   135  	err = modifyFn(requirements, chart, values, templates, chartDir, details)
   136  	if err != nil {
   137  		return err
   138  	}
   139  
   140  	err = helm.SaveFile(requirementsFile, requirements)
   141  	if err != nil {
   142  		return err
   143  	}
   144  
   145  	err = helm.SaveFile(chartFile, chart)
   146  	if err != nil {
   147  		return err
   148  	}
   149  	return nil
   150  }
   151  
   152  // CreateUpgradeRequirementsFn creates the ModifyChartFn that upgrades the requirements of a chart.
   153  // Either all requirements may be upgraded, or the chartName,
   154  // alias and version can be specified. A username and password can be passed for a protected repository.
   155  // The passed inspectChartFunc will be called whilst the chart for each requirement is unpacked on the disk.
   156  // Operations are carried out using the helmer interface and there will be more logging if verbose is true.
   157  // The passed valuesFiles are used to add a values.yaml to each requirement.
   158  func CreateUpgradeRequirementsFn(all bool, chartName string, alias string, version string, username string,
   159  	password string, helmer helm.Helmer, inspectChartFunc func(chartDir string,
   160  		existingValues map[string]interface{}) error, verbose bool, valuesFiles *ValuesFiles) ModifyChartFn {
   161  	upgraded := false
   162  	return func(requirements *helm.Requirements, metadata *chart.Metadata, values map[string]interface{},
   163  		templates map[string]string, envDir string, details *gits.PullRequestDetails) error {
   164  
   165  		// Work through the upgrades
   166  		for _, d := range requirements.Dependencies {
   167  			// We need to ignore the platform unless the chart name is the platform
   168  			upgrade := false
   169  			if all {
   170  				if d.Name != "jenkins-x-platform" {
   171  					upgrade = true
   172  				}
   173  			} else {
   174  				if d.Name == chartName && (d.Alias == "" || d.Alias == alias) {
   175  					upgrade = true
   176  				}
   177  			}
   178  			if upgrade {
   179  				upgraded = true
   180  
   181  				oldVersion := d.Version
   182  				err := helm.InspectChart(d.Name, version, d.Repository, username, password, helmer,
   183  					func(chartDir string) error {
   184  						if all || version == "" {
   185  							// Upgrade to the latest version
   186  							_, chartVersion, err := helm.LoadChartNameAndVersion(filepath.Join(chartDir, "Chart.yaml"))
   187  							if err != nil {
   188  								return errors.Wrapf(err, "error loading chart from %s", chartDir)
   189  							}
   190  							version = chartVersion
   191  							if verbose {
   192  								log.Logger().Infof("No version specified so using latest version which is %s", util.ColorInfo(version))
   193  							}
   194  						}
   195  
   196  						err := inspectChartFunc(chartDir, values)
   197  						if err != nil {
   198  							return errors.Wrapf(err, "running inspectChartFunc for %s", d.Name)
   199  						}
   200  						err = CreateNestedRequirementDir(envDir, chartName, chartDir, version, d.Repository, verbose,
   201  							valuesFiles, helmer)
   202  						if err != nil {
   203  							return errors.Wrapf(err, "creating nested app dir in chart dir %s", chartDir)
   204  						}
   205  						return nil
   206  					})
   207  				if err != nil {
   208  					return errors.Wrapf(err, "inspecting chart %s", d.Name)
   209  				}
   210  
   211  				// Do the upgrade
   212  				d.Version = version
   213  				if !all {
   214  					details.Title = fmt.Sprintf("Upgrade %s to %s", chartName, version)
   215  					details.Message = fmt.Sprintf("Upgrade %s from %s to %s", chartName, oldVersion, version)
   216  				} else {
   217  					details.Message = fmt.Sprintf("%s\n* %s from %s to %s", details.Message, d.Name, oldVersion, version)
   218  				}
   219  			}
   220  		}
   221  		if !upgraded {
   222  			log.Logger().Infof("No upgrades available")
   223  		}
   224  		return nil
   225  	}
   226  }
   227  
   228  // CreateAddRequirementFn create the ModifyChartFn that adds a dependency to a chart. It takes the chart name,
   229  // an alias for the chart, the version of the chart, the repo to load the chart from,
   230  // valuesFiles (an array of paths to values.yaml files to add). The chartDir is the unpacked chart being added,
   231  // which is used to add extra metadata about the chart (e.g. the charts readme, the release.yaml, the git repo url and
   232  // the release notes) - if this points to a non-existent directory it will be ignored.
   233  func CreateAddRequirementFn(chartName string, alias string, version string, repo string,
   234  	valuesFiles *ValuesFiles, chartDir string, verbose bool, helmer helm.Helmer) ModifyChartFn {
   235  	return func(requirements *helm.Requirements, chart *helmchart.Metadata, values map[string]interface{},
   236  		templates map[string]string, envDir string, details *gits.PullRequestDetails) error {
   237  		// See if the chart already exists in requirements
   238  		found := false
   239  		for _, d := range requirements.Dependencies {
   240  			if d.Name == chartName && d.Alias == alias {
   241  				// App found
   242  				log.Logger().Infof("App %s already installed.", util.ColorWarning(chartName))
   243  				if version != d.Version {
   244  					log.Logger().Infof("To upgrade the chartName use %s or %s",
   245  						util.ColorInfo("jx upgrade chartName <chartName>"),
   246  						util.ColorInfo("jx upgrade apps --all"))
   247  				}
   248  				found = true
   249  				break
   250  			}
   251  		}
   252  		// If chartName not found, add it
   253  		if !found {
   254  			requirements.Dependencies = append(requirements.Dependencies, &helm.Dependency{
   255  				Alias:      alias,
   256  				Repository: repo,
   257  				Name:       chartName,
   258  				Version:    version,
   259  			})
   260  			err := CreateNestedRequirementDir(envDir, chartName, chartDir, version, repo, verbose, valuesFiles, helmer)
   261  			if err != nil {
   262  				return errors.Wrapf(err, "creating nested app dir in chart dir %s", chartDir)
   263  			}
   264  
   265  		}
   266  		return nil
   267  	}
   268  }
   269  
   270  // CreateNestedRequirementDir creates the a directory for a chart being added as a requirement, adding a README.md,
   271  // the release.yaml, and the values.yaml. The dir is the unpacked chart directory to which the requirement is being
   272  // added. The requirementName, requirementVersion,
   273  // requirementRepository and requirementValuesFiles are used to construct the metadata,
   274  // as well as info in the requirementDir which points to the unpacked chart of the requirement.
   275  func CreateNestedRequirementDir(dir string, requirementName string, requirementDir string, requirementVersion string,
   276  	requirementRepository string, verbose bool, requirementValuesFiles *ValuesFiles, helmer helm.Helmer) error {
   277  	appDir := filepath.Join(dir, requirementName)
   278  	rootValuesFileName := filepath.Join(appDir, helm.ValuesFileName)
   279  	err := os.MkdirAll(appDir, 0700)
   280  	if err != nil {
   281  		return errors.Wrapf(err, "cannot create requirementName directory %s", appDir)
   282  	}
   283  	if verbose {
   284  		log.Logger().Infof("Using %s for requirementName files", appDir)
   285  	}
   286  	if requirementValuesFiles != nil && len(requirementValuesFiles.Items) > 0 {
   287  		if len(requirementValuesFiles.Items) == 1 {
   288  			// We need to write the values file into the right spot for the requirementName
   289  			err = util.CopyFile(requirementValuesFiles.Items[0], rootValuesFileName)
   290  			if err != nil {
   291  				return errors.Wrapf(err, "cannot copy values."+
   292  					"yaml to %s directory %s", requirementName, appDir)
   293  			}
   294  		} else {
   295  			var sb strings.Builder
   296  			for _, fileName := range requirementValuesFiles.Items {
   297  				data, err := ioutil.ReadFile(fileName)
   298  				if err != nil {
   299  					return errors.Wrapf(err, "failed to load values.yaml file %s", fileName)
   300  				}
   301  				_, err = sb.Write(data)
   302  				if err != nil {
   303  					return errors.Wrapf(err, "failed to append values.yaml file %s to buffer", fileName)
   304  				}
   305  				if !strings.HasSuffix(sb.String(), "\n") {
   306  					sb.WriteString("\n")
   307  				}
   308  			}
   309  			err = ioutil.WriteFile(rootValuesFileName, []byte(sb.String()), util.DefaultWritePermissions)
   310  			if err != nil {
   311  				return errors.Wrapf(err, "failed to write values.yaml file %s", rootValuesFileName)
   312  			}
   313  		}
   314  		if verbose {
   315  			log.Logger().Infof("Writing values file to %s", rootValuesFileName)
   316  		}
   317  	}
   318  	// Write the release.yaml
   319  	var gitRepo, releaseNotesURL, appReadme, description string
   320  	templatesDir := filepath.Join(requirementDir, "templates")
   321  	if _, err := os.Stat(templatesDir); os.IsNotExist(err) {
   322  		if verbose {
   323  			log.Logger().Infof("No templates directory exists in %s", util.ColorInfo(dir))
   324  		}
   325  	} else if err != nil {
   326  		return errors.Wrapf(err, "stat directory %s", appDir)
   327  	} else {
   328  		releaseYamlPath := filepath.Join(templatesDir, "release.yaml")
   329  		if _, err := os.Stat(releaseYamlPath); err == nil {
   330  			bytes, err := ioutil.ReadFile(releaseYamlPath)
   331  			if err != nil {
   332  				return errors.Wrapf(err, "release.yaml from %s", templatesDir)
   333  			}
   334  			release := jenkinsv1.Release{}
   335  			err = yaml.Unmarshal(bytes, &release)
   336  			if err != nil {
   337  				return errors.Wrapf(err, "unmarshal %s", releaseYamlPath)
   338  			}
   339  			gitRepo = release.Spec.GitHTTPURL
   340  			releaseNotesURL = release.Spec.ReleaseNotesURL
   341  			releaseYamlOutPath := filepath.Join(appDir, "release.yaml")
   342  			err = ioutil.WriteFile(releaseYamlOutPath, bytes, 0600)
   343  			if err != nil {
   344  				return errors.Wrapf(err, "write file %s", releaseYamlOutPath)
   345  			}
   346  			if verbose {
   347  				log.Logger().Infof("Read release notes URL %s and git repo url %s from release.yaml\nWriting release."+
   348  					"yaml from chartName to %s", releaseNotesURL, gitRepo, releaseYamlOutPath)
   349  			}
   350  		} else if os.IsNotExist(err) {
   351  			if verbose {
   352  
   353  				log.Logger().Infof("Not adding release.yaml as not present in chart. Only files in %s are:",
   354  					templatesDir)
   355  				err := util.ListDirectory(templatesDir, true)
   356  				if err != nil {
   357  					return err
   358  				}
   359  			}
   360  		} else {
   361  			return errors.Wrapf(err, "reading release.yaml from %s", templatesDir)
   362  		}
   363  	}
   364  	chartYamlPath := filepath.Join(requirementDir, helm.ChartFileName)
   365  	if _, err := os.Stat(chartYamlPath); err == nil {
   366  		bytes, err := ioutil.ReadFile(chartYamlPath)
   367  		if err != nil {
   368  			return errors.Wrapf(err, "read %s from %s", helm.ChartFileName, requirementDir)
   369  		}
   370  		chart := helmchart.Metadata{}
   371  		err = yaml.Unmarshal(bytes, &chart)
   372  		if err != nil {
   373  			return errors.Wrapf(err, "unmarshal %s", chartYamlPath)
   374  		}
   375  		description = chart.Description
   376  	} else if os.IsNotExist(err) {
   377  		if verbose {
   378  			log.Logger().Infof("Not adding %s as not present in chart. Only files in %s are:", helm.ChartFileName,
   379  				requirementDir)
   380  			err := util.ListDirectory(requirementDir, true)
   381  			if err != nil {
   382  				return err
   383  			}
   384  		}
   385  	} else {
   386  		return errors.Wrapf(err, "stat Chart.yaml from %s", requirementDir)
   387  	}
   388  	// Need to copy over any referenced files, and their schemas
   389  	rootValues, err := helm.LoadValuesFile(rootValuesFileName)
   390  	if err != nil {
   391  		return err
   392  	}
   393  	schemas := make(map[string][]string)
   394  	possibles := make(map[string]string)
   395  	if _, err := os.Stat(requirementDir); err == nil {
   396  		files, err := ioutil.ReadDir(requirementDir)
   397  		if err != nil {
   398  			return errors.Wrapf(err, "unable to list files in %s", requirementDir)
   399  		}
   400  		possibleReadmes := make([]string, 0)
   401  		for _, file := range files {
   402  			fileName := strings.ToUpper(file.Name())
   403  			if fileName == "README.MD" || fileName == "README" {
   404  				possibleReadmes = append(possibleReadmes, filepath.Join(requirementDir, file.Name()))
   405  			}
   406  		}
   407  		if len(possibleReadmes) > 1 {
   408  			if verbose {
   409  				log.Logger().Warnf("Unable to add README to PR for %s as more than one exists and not sure which to"+
   410  					" use %s", requirementName, possibleReadmes)
   411  			}
   412  		} else if len(possibleReadmes) == 1 {
   413  			bytes, err := ioutil.ReadFile(possibleReadmes[0])
   414  			if err != nil {
   415  				return errors.Wrapf(err, "unable to read file %s", possibleReadmes[0])
   416  			}
   417  			appReadme = string(bytes)
   418  		}
   419  
   420  		for _, f := range files {
   421  			ignore, err := util.IgnoreFile(f.Name(), helm.DefaultValuesTreeIgnores)
   422  			if err != nil {
   423  				return err
   424  			}
   425  			if !f.IsDir() && !ignore {
   426  				key := f.Name()
   427  				// Handle .schema. files specially
   428  				if parts := strings.Split(key, ".schema."); len(parts) > 1 {
   429  					// this is a file named *.schema.*, the part before .schema is the key
   430  					if _, ok := schemas[parts[0]]; !ok {
   431  						schemas[parts[0]] = make([]string, 0)
   432  					}
   433  					schemas[parts[0]] = append(schemas[parts[0]], filepath.Join(requirementDir, f.Name()))
   434  				}
   435  				possibles[key] = filepath.Join(requirementDir, f.Name())
   436  
   437  			}
   438  		}
   439  	} else if !os.IsNotExist(err) {
   440  		return errors.Wrap(err, fmt.Sprintf("error reading %s", requirementDir))
   441  	}
   442  	if verbose && appReadme == "" {
   443  		log.Logger().Infof("Not adding App Readme as no README, README.md, readme or readme.md found in %s", requirementDir)
   444  	}
   445  	app, filename, err := LocateAppResource(helmer, requirementDir, requirementName)
   446  	if err != nil {
   447  		return errors.WithStack(err)
   448  	}
   449  	err = EnhanceChartWithAppMetadata(requirementDir, app, requirementRepository, appDir, filename)
   450  	if err != nil {
   451  		return errors.WithStack(err)
   452  	}
   453  	readme := helm.GenerateReadmeForChart(requirementName, requirementVersion, description, requirementRepository, gitRepo, releaseNotesURL, appReadme)
   454  	readmeOutPath := filepath.Join(appDir, "README.MD")
   455  	err = ioutil.WriteFile(readmeOutPath, []byte(readme), 0600)
   456  	if err != nil {
   457  		return errors.Wrapf(err, "write README.md to %s", appDir)
   458  	}
   459  	if verbose {
   460  		log.Logger().Infof("Writing README.md to %s", readmeOutPath)
   461  	}
   462  	externalFileHandler := func(path string, element map[string]interface{}, key string) error {
   463  		fileName, _ := filepath.Split(path)
   464  		err := util.CopyFile(path, filepath.Join(appDir, fileName))
   465  		if err != nil {
   466  			return errors.Wrapf(err, "copy %s to %s", path, appDir)
   467  		}
   468  		// key for schema is the filename without the extension
   469  		schemaKey := strings.TrimSuffix(fileName, filepath.Ext(fileName))
   470  		if schemaPaths, ok := schemas[schemaKey]; ok {
   471  			for _, schemaPath := range schemaPaths {
   472  				fileName, _ := filepath.Split(schemaPath)
   473  				schemaOutPath := filepath.Join(appDir, fileName)
   474  				err := util.CopyFile(schemaPath, schemaOutPath)
   475  				if err != nil {
   476  					return errors.Wrapf(err, "copy %s to %s", schemaPath, appDir)
   477  				}
   478  				if verbose {
   479  					log.Logger().Infof("Writing %s to %s", fileName, schemaOutPath)
   480  				}
   481  			}
   482  		}
   483  		return nil
   484  	}
   485  	err = helm.HandleExternalFileRefs(rootValues, possibles, "", externalFileHandler)
   486  	if err != nil {
   487  		return err
   488  	}
   489  
   490  	return nil
   491  }
   492  
   493  // EnhanceChartWithAppMetadata will update the app in chartDir with app metadata,
   494  // writing the custom resource to the outputDir as a new file called filename
   495  func EnhanceChartWithAppMetadata(chartDir string, app *jenkinsv1.App, repository string, outputDir string,
   496  	filename string) error {
   497  	outputTemplateDir := filepath.Join(outputDir, "templates")
   498  	templatesDirExists, err := util.DirExists(outputTemplateDir)
   499  	if err != nil {
   500  		return err
   501  	}
   502  	if !templatesDirExists {
   503  		err = os.Mkdir(outputTemplateDir, os.ModePerm)
   504  		if err != nil {
   505  			return errors.Wrapf(err, "creating directory %s", outputTemplateDir)
   506  		}
   507  	}
   508  	outputFilename := filepath.Join(outputTemplateDir, filename)
   509  	err = AddAppMetaData(chartDir, app, repository)
   510  	if err != nil {
   511  		return errors.Wrapf(err, "enhancing %s with app metadata", app.Name)
   512  	}
   513  	err = helm.SaveFile(outputFilename, app)
   514  	if err != nil {
   515  		return errors.Wrapf(err, "saving enhanced app metadata to %s for app %s", outputFilename, app.Name)
   516  	}
   517  	return nil
   518  }
   519  
   520  // AddAppMetaData applies chart metadata to an App resource
   521  func AddAppMetaData(chartDir string, app *jenkinsv1.App, repository string) error {
   522  	metadata, err := helm.LoadChartFile(filepath.Join(chartDir, "Chart.yaml"))
   523  	if err != nil {
   524  		return errors.Wrapf(err, "error loading chart from %s", chartDir)
   525  	}
   526  	if app.Annotations == nil {
   527  		app.Annotations = make(map[string]string)
   528  	}
   529  	app.Annotations[helm.AnnotationAppDescription] = metadata.GetDescription()
   530  	if _, err = url.Parse(repository); err != nil {
   531  		return errors.Wrap(err, "Invalid repository url")
   532  	}
   533  	app.Annotations[helm.AnnotationAppRepository] = util.SanitizeURL(repository)
   534  	if app.Labels == nil {
   535  		app.Labels = make(map[string]string)
   536  	}
   537  	app.Labels[helm.LabelAppName] = metadata.Name
   538  	app.Labels[helm.LabelAppVersion] = metadata.Version
   539  	return nil
   540  }
   541  
   542  // LocateAppResource finds or creates a resource of Kind: App in a given appName rooted in chartDir,
   543  // writing it to outputDir. The template with the
   544  func LocateAppResource(helmer helm.Helmer, chartDir string, appName string) (*jenkinsv1.App,
   545  	string, error) {
   546  
   547  	templateWorkDir := filepath.Join(chartDir, "output")
   548  	templateWorkDirExists, err := util.DirExists(templateWorkDir)
   549  	if err != nil {
   550  		return nil, "", err
   551  	}
   552  	if !templateWorkDirExists {
   553  		err = os.Mkdir(templateWorkDir, os.ModePerm)
   554  		if err != nil {
   555  			return nil, "", errors.Wrapf(err, "creating template work dir %s", templateWorkDir)
   556  		}
   557  	}
   558  	defaultApp := &jenkinsv1.App{
   559  		TypeMeta: metav1.TypeMeta{
   560  			Kind:       "App",
   561  			APIVersion: jenkinsio.GroupName + "/" + jenkinsio.Version,
   562  		},
   563  		ObjectMeta: metav1.ObjectMeta{
   564  			Name: appName,
   565  		},
   566  		Spec: jenkinsv1.AppSpec{},
   567  	}
   568  	err = helmer.Template(chartDir, appName, "", templateWorkDir, false, make([]string, 0), make([]string, 0), make([]string, 0))
   569  	if err != nil {
   570  		templateWorkDir = chartDir
   571  	}
   572  	completedTemplatesDir := filepath.Join(templateWorkDir, appName, "templates")
   573  	templates, _ := ioutil.ReadDir(completedTemplatesDir)
   574  
   575  	filename := "app.yaml"
   576  	possibles := make([]string, 0)
   577  	app := &jenkinsv1.App{}
   578  	for _, template := range templates {
   579  		if template.IsDir() {
   580  			continue
   581  		}
   582  		appBytes, err := ioutil.ReadFile(filepath.Join(completedTemplatesDir, template.Name()))
   583  		if err != nil {
   584  			return nil, "", errors.Wrapf(err, "reading file %s", filename)
   585  		}
   586  		err = yaml.Unmarshal(appBytes, app)
   587  		if err == nil {
   588  			if app.Kind == "App" {
   589  				// Use the first located resource
   590  				filename = template.Name()
   591  				possibles = append(possibles, app.Name)
   592  			}
   593  		}
   594  	}
   595  
   596  	switch size := len(possibles); {
   597  	case size > 1:
   598  		return nil, "", errors.Errorf("at most one resource of Kind: App can be specified but found %v", possibles)
   599  	case size == 0:
   600  		//If we are adding a generated app, we need the placeholder to be the App object, otherwise a random one
   601  		//from templates is going to be used instead
   602  		app = defaultApp
   603  	}
   604  
   605  	return app, filename, nil
   606  }