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 }