github.com/asifdxtreme/cli@v6.1.3-0.20150123051144-9ead8700b4ae+incompatible/cf/commands/application/push.go (about) 1 package application 2 3 import ( 4 "fmt" 5 "os" 6 "regexp" 7 "strconv" 8 "strings" 9 10 . "github.com/cloudfoundry/cli/cf/i18n" 11 "github.com/cloudfoundry/cli/fileutils" 12 13 "github.com/cloudfoundry/cli/cf/actors" 14 "github.com/cloudfoundry/cli/cf/api" 15 "github.com/cloudfoundry/cli/cf/api/applications" 16 "github.com/cloudfoundry/cli/cf/api/authentication" 17 "github.com/cloudfoundry/cli/cf/api/stacks" 18 "github.com/cloudfoundry/cli/cf/app_files" 19 "github.com/cloudfoundry/cli/cf/command_metadata" 20 "github.com/cloudfoundry/cli/cf/commands/service" 21 "github.com/cloudfoundry/cli/cf/configuration/core_config" 22 "github.com/cloudfoundry/cli/cf/errors" 23 "github.com/cloudfoundry/cli/cf/flag_helpers" 24 "github.com/cloudfoundry/cli/cf/formatters" 25 "github.com/cloudfoundry/cli/cf/manifest" 26 "github.com/cloudfoundry/cli/cf/models" 27 "github.com/cloudfoundry/cli/cf/requirements" 28 "github.com/cloudfoundry/cli/cf/terminal" 29 "github.com/cloudfoundry/cli/words/generator" 30 "github.com/codegangsta/cli" 31 ) 32 33 type Push struct { 34 ui terminal.UI 35 config core_config.Reader 36 manifestRepo manifest.ManifestRepository 37 appStarter ApplicationStarter 38 appStopper ApplicationStopper 39 serviceBinder service.ServiceBinder 40 appRepo applications.ApplicationRepository 41 domainRepo api.DomainRepository 42 routeRepo api.RouteRepository 43 serviceRepo api.ServiceRepository 44 stackRepo stacks.StackRepository 45 authRepo authentication.AuthenticationRepository 46 wordGenerator generator.WordGenerator 47 actor actors.PushActor 48 zipper app_files.Zipper 49 app_files app_files.AppFiles 50 } 51 52 func NewPush(ui terminal.UI, config core_config.Reader, manifestRepo manifest.ManifestRepository, 53 starter ApplicationStarter, stopper ApplicationStopper, binder service.ServiceBinder, 54 appRepo applications.ApplicationRepository, domainRepo api.DomainRepository, routeRepo api.RouteRepository, 55 stackRepo stacks.StackRepository, serviceRepo api.ServiceRepository, 56 authRepo authentication.AuthenticationRepository, wordGenerator generator.WordGenerator, 57 actor actors.PushActor, zipper app_files.Zipper, app_files app_files.AppFiles) *Push { 58 return &Push{ 59 ui: ui, 60 config: config, 61 manifestRepo: manifestRepo, 62 appStarter: starter, 63 appStopper: stopper, 64 serviceBinder: binder, 65 appRepo: appRepo, 66 domainRepo: domainRepo, 67 routeRepo: routeRepo, 68 serviceRepo: serviceRepo, 69 stackRepo: stackRepo, 70 authRepo: authRepo, 71 wordGenerator: wordGenerator, 72 actor: actor, 73 zipper: zipper, 74 app_files: app_files, 75 } 76 } 77 78 func (cmd *Push) Metadata() command_metadata.CommandMetadata { 79 return command_metadata.CommandMetadata{ 80 Name: "push", 81 ShortName: "p", 82 Description: T("Push a new app or sync changes to an existing app"), 83 Usage: T("Push a single app (with or without a manifest):\n") + T(" CF_NAME push APP_NAME [-b BUILDPACK_NAME] [-c COMMAND] [-d DOMAIN] [-f MANIFEST_PATH]\n") + T(" [-i NUM_INSTANCES] [-k DISK] [-m MEMORY] [-n HOST] [-p PATH] [-s STACK] [-t TIMEOUT]\n") + 84 " [--no-hostname] [--no-manifest] [--no-route] [--no-start]\n" + 85 "\n" + T(" Push multiple apps with a manifest:\n") + T(" CF_NAME push [-f MANIFEST_PATH]\n"), 86 Flags: []cli.Flag{ 87 flag_helpers.NewStringFlag("b", T("Custom buildpack by name (e.g. my-buildpack) or GIT URL (e.g. 'https://github.com/heroku/heroku-buildpack-play.git') or GIT BRANCH URL (e.g. 'https://github.com/heroku/heroku-buildpack-play.git#develop' for 'develop' branch) ")), 88 flag_helpers.NewStringFlag("c", T("Startup command, set to null to reset to default start command")), 89 flag_helpers.NewStringFlag("d", T("Domain (e.g. example.com)")), 90 flag_helpers.NewStringFlag("f", T("Path to manifest")), 91 flag_helpers.NewIntFlag("i", T("Number of instances")), 92 flag_helpers.NewStringFlag("k", T("Disk limit (e.g. 256M, 1024M, 1G)")), 93 flag_helpers.NewStringFlag("m", T("Memory limit (e.g. 256M, 1024M, 1G)")), 94 flag_helpers.NewStringFlag("n", T("Hostname (e.g. my-subdomain)")), 95 flag_helpers.NewStringFlag("p", T("Path to app directory or file")), 96 flag_helpers.NewStringFlag("s", T("Stack to use (a stack is a pre-built file system, including an operating system, that can run apps)")), 97 flag_helpers.NewStringFlag("t", T("Maximum time (in seconds) for CLI to wait for application start, other server side timeouts may apply")), 98 cli.BoolFlag{Name: "no-hostname", Usage: T("Map the root domain to this app")}, 99 cli.BoolFlag{Name: "no-manifest", Usage: T("Ignore manifest file")}, 100 cli.BoolFlag{Name: "no-route", Usage: T("Do not map a route to this app and remove routes from previous pushes of this app.")}, 101 cli.BoolFlag{Name: "no-start", Usage: T("Do not start an app after pushing")}, 102 cli.BoolFlag{Name: "random-route", Usage: T("Create a random route for this app")}, 103 }, 104 } 105 } 106 107 func (cmd *Push) GetRequirements(requirementsFactory requirements.Factory, c *cli.Context) (reqs []requirements.Requirement, err error) { 108 if len(c.Args()) > 1 { 109 cmd.ui.FailWithUsage(c) 110 } 111 112 reqs = []requirements.Requirement{ 113 requirementsFactory.NewLoginRequirement(), 114 requirementsFactory.NewTargetedSpaceRequirement(), 115 } 116 return 117 } 118 119 func (cmd *Push) Run(c *cli.Context) { 120 appSet := cmd.findAndValidateAppsToPush(c) 121 _, apiErr := cmd.authRepo.RefreshAuthToken() 122 if apiErr != nil { 123 cmd.ui.Failed(apiErr.Error()) 124 return 125 } 126 127 routeActor := actors.NewRouteActor(cmd.ui, cmd.routeRepo) 128 noHostname := c.Bool("no-hostname") 129 130 for _, appParams := range appSet { 131 cmd.fetchStackGuid(&appParams) 132 app := cmd.createOrUpdateApp(appParams) 133 134 cmd.updateRoutes(routeActor, app, appParams, noHostname) 135 136 cmd.ui.Say(T("Uploading {{.AppName}}...", 137 map[string]interface{}{"AppName": terminal.EntityNameColor(app.Name)})) 138 139 apiErr := cmd.uploadApp(app.Guid, *appParams.Path) 140 if apiErr != nil { 141 cmd.ui.Failed(fmt.Sprintf(T("Error uploading application.\n{{.ApiErr}}", 142 map[string]interface{}{"ApiErr": apiErr.Error()}))) 143 return 144 } 145 cmd.ui.Ok() 146 147 if appParams.ServicesToBind != nil { 148 cmd.bindAppToServices(*appParams.ServicesToBind, app) 149 } 150 151 cmd.restart(app, appParams, c) 152 } 153 } 154 155 func (cmd *Push) updateRoutes(routeActor actors.RouteActor, app models.Application, appParams models.AppParams, noHostName bool) { 156 defaultRouteAcceptable := len(app.Routes) == 0 157 routeDefined := appParams.Domain != nil || !appParams.IsHostEmpty() || noHostName 158 159 if appParams.NoRoute { 160 cmd.removeRoutes(app, routeActor) 161 return 162 } 163 164 if routeDefined || defaultRouteAcceptable { 165 if appParams.IsHostEmpty() { 166 cmd.createAndBindRoute(nil, appParams, routeActor, app, noHostName) 167 } else { 168 for _, host := range *(appParams.Hosts) { 169 cmd.createAndBindRoute(&host, appParams, routeActor, app, noHostName) 170 } 171 } 172 } 173 } 174 175 func (cmd *Push) createAndBindRoute(host *string, appParams models.AppParams, routeActor actors.RouteActor, app models.Application, noHostName bool) { 176 domain := cmd.findDomain(appParams.Domain) 177 hostname := cmd.hostnameForApp(host, appParams.UseRandomHostname, app.Name, noHostName) 178 route := routeActor.FindOrCreateRoute(hostname, domain) 179 routeActor.BindRoute(app, route) 180 } 181 182 func (cmd *Push) removeRoutes(app models.Application, routeActor actors.RouteActor) { 183 if len(app.Routes) == 0 { 184 cmd.ui.Say(T("App {{.AppName}} is a worker, skipping route creation", 185 map[string]interface{}{"AppName": terminal.EntityNameColor(app.Name)})) 186 } else { 187 routeActor.UnbindAll(app) 188 } 189 } 190 191 func (cmd *Push) hostnameForApp(host *string, useRandomHostName bool, name string, noHostName bool) string { 192 if noHostName { 193 return "" 194 } 195 196 if host != nil { 197 return *host 198 } else if useRandomHostName { 199 return hostNameForString(name) + "-" + cmd.wordGenerator.Babble() 200 } else { 201 return hostNameForString(name) 202 } 203 } 204 205 var forbiddenHostCharRegex = regexp.MustCompile("[^a-z0-9-]") 206 var whitespaceRegex = regexp.MustCompile(`[\s_]+`) 207 208 func hostNameForString(name string) string { 209 name = strings.ToLower(name) 210 name = whitespaceRegex.ReplaceAllString(name, "-") 211 name = forbiddenHostCharRegex.ReplaceAllString(name, "") 212 return name 213 } 214 215 func (cmd *Push) findDomain(domainName *string) (domain models.DomainFields) { 216 domain, error := cmd.domainRepo.FirstOrDefault(cmd.config.OrganizationFields().Guid, domainName) 217 if error != nil { 218 cmd.ui.Failed(error.Error()) 219 } 220 221 return 222 } 223 224 func (cmd *Push) bindAppToServices(services []string, app models.Application) { 225 for _, serviceName := range services { 226 serviceInstance, err := cmd.serviceRepo.FindInstanceByName(serviceName) 227 228 if err != nil { 229 cmd.ui.Failed(T("Could not find service {{.ServiceName}} to bind to {{.AppName}}", 230 map[string]interface{}{"ServiceName": serviceName, "AppName": app.Name})) 231 return 232 } 233 234 cmd.ui.Say(T("Binding service {{.ServiceName}} to app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...", 235 map[string]interface{}{ 236 "ServiceName": terminal.EntityNameColor(serviceInstance.Name), 237 "AppName": terminal.EntityNameColor(app.Name), 238 "OrgName": terminal.EntityNameColor(cmd.config.OrganizationFields().Name), 239 "SpaceName": terminal.EntityNameColor(cmd.config.SpaceFields().Name), 240 "Username": terminal.EntityNameColor(cmd.config.Username())})) 241 242 err = cmd.serviceBinder.BindApplication(app, serviceInstance) 243 244 switch httpErr := err.(type) { 245 case errors.HttpError: 246 if httpErr.ErrorCode() == errors.APP_ALREADY_BOUND { 247 err = nil 248 } 249 } 250 251 if err != nil { 252 cmd.ui.Failed(T("Could not bind to service {{.ServiceName}}\nError: {{.Err}}", 253 map[string]interface{}{"ServiceName": serviceName, "Err": err.Error()})) 254 } 255 256 cmd.ui.Ok() 257 } 258 } 259 260 func (cmd *Push) fetchStackGuid(appParams *models.AppParams) { 261 if appParams.StackName == nil { 262 return 263 } 264 265 stackName := *appParams.StackName 266 cmd.ui.Say(T("Using stack {{.StackName}}...", 267 map[string]interface{}{"StackName": terminal.EntityNameColor(stackName)})) 268 269 stack, apiErr := cmd.stackRepo.FindByName(stackName) 270 if apiErr != nil { 271 cmd.ui.Failed(apiErr.Error()) 272 return 273 } 274 275 cmd.ui.Ok() 276 appParams.StackGuid = &stack.Guid 277 } 278 279 func (cmd *Push) restart(app models.Application, params models.AppParams, c *cli.Context) { 280 if app.State != T("stopped") { 281 cmd.ui.Say("") 282 app, _ = cmd.appStopper.ApplicationStop(app, cmd.config.OrganizationFields().Name, cmd.config.SpaceFields().Name) 283 } 284 285 cmd.ui.Say("") 286 287 if c.Bool("no-start") { 288 return 289 } 290 291 if params.HealthCheckTimeout != nil { 292 cmd.appStarter.SetStartTimeoutInSeconds(*params.HealthCheckTimeout) 293 } 294 295 cmd.appStarter.ApplicationStart(app, cmd.config.OrganizationFields().Name, cmd.config.SpaceFields().Name) 296 } 297 298 func (cmd *Push) createOrUpdateApp(appParams models.AppParams) (app models.Application) { 299 if appParams.Name == nil { 300 cmd.ui.Failed(T("Error: No name found for app")) 301 } 302 303 app, apiErr := cmd.appRepo.Read(*appParams.Name) 304 305 switch apiErr.(type) { 306 case nil: 307 app = cmd.updateApp(app, appParams) 308 case *errors.ModelNotFoundError: 309 app = cmd.createApp(appParams) 310 default: 311 cmd.ui.Failed(apiErr.Error()) 312 } 313 314 return 315 } 316 317 func (cmd *Push) createApp(appParams models.AppParams) (app models.Application) { 318 spaceGuid := cmd.config.SpaceFields().Guid 319 appParams.SpaceGuid = &spaceGuid 320 321 cmd.ui.Say(T("Creating app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...", 322 map[string]interface{}{ 323 "AppName": terminal.EntityNameColor(*appParams.Name), 324 "OrgName": terminal.EntityNameColor(cmd.config.OrganizationFields().Name), 325 "SpaceName": terminal.EntityNameColor(cmd.config.SpaceFields().Name), 326 "Username": terminal.EntityNameColor(cmd.config.Username())})) 327 328 app, apiErr := cmd.appRepo.Create(appParams) 329 if apiErr != nil { 330 cmd.ui.Failed(apiErr.Error()) 331 } 332 333 cmd.ui.Ok() 334 cmd.ui.Say("") 335 336 return 337 } 338 339 func (cmd *Push) updateApp(app models.Application, appParams models.AppParams) (updatedApp models.Application) { 340 cmd.ui.Say(T("Updating app {{.AppName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...", 341 map[string]interface{}{ 342 "AppName": terminal.EntityNameColor(app.Name), 343 "OrgName": terminal.EntityNameColor(cmd.config.OrganizationFields().Name), 344 "SpaceName": terminal.EntityNameColor(cmd.config.SpaceFields().Name), 345 "Username": terminal.EntityNameColor(cmd.config.Username())})) 346 347 if appParams.EnvironmentVars != nil { 348 for key, val := range app.EnvironmentVars { 349 if _, ok := (*appParams.EnvironmentVars)[key]; !ok { 350 (*appParams.EnvironmentVars)[key] = val 351 } 352 } 353 } 354 355 var apiErr error 356 updatedApp, apiErr = cmd.appRepo.Update(app.Guid, appParams) 357 if apiErr != nil { 358 cmd.ui.Failed(apiErr.Error()) 359 } 360 361 cmd.ui.Ok() 362 cmd.ui.Say("") 363 364 return 365 } 366 367 func (cmd *Push) findAndValidateAppsToPush(c *cli.Context) []models.AppParams { 368 appsFromManifest := cmd.getAppParamsFromManifest(c) 369 appFromContext := cmd.getAppParamsFromContext(c) 370 return cmd.createAppSetFromContextAndManifest(appFromContext, appsFromManifest) 371 } 372 373 func (cmd *Push) getAppParamsFromManifest(c *cli.Context) []models.AppParams { 374 if c.Bool("no-manifest") { 375 return []models.AppParams{} 376 } 377 378 var path string 379 if c.String("f") != "" { 380 path = c.String("f") 381 } else { 382 var err error 383 path, err = os.Getwd() 384 if err != nil { 385 cmd.ui.Failed(T("Could not determine the current working directory!"), err) 386 } 387 } 388 389 m, err := cmd.manifestRepo.ReadManifest(path) 390 391 if err != nil { 392 if m.Path == "" && c.String("f") == "" { 393 return []models.AppParams{} 394 } else { 395 cmd.ui.Failed(T("Error reading manifest file:\n{{.Err}}", map[string]interface{}{"Err": err.Error()})) 396 } 397 } 398 399 apps, err := m.Applications() 400 if err != nil { 401 cmd.ui.Failed("Error reading manifest file:\n%s", err) 402 } 403 404 cmd.ui.Say(T("Using manifest file {{.Path}}\n", 405 map[string]interface{}{"Path": terminal.EntityNameColor(m.Path)})) 406 return apps 407 } 408 409 func (cmd *Push) createAppSetFromContextAndManifest(contextApp models.AppParams, manifestApps []models.AppParams) (apps []models.AppParams) { 410 var err error 411 412 switch len(manifestApps) { 413 case 0: 414 if contextApp.Name == nil { 415 err = errors.New(T("Manifest file is not found in the current directory, please provide either an app name or manifest")) 416 } else { 417 err = addApp(&apps, contextApp) 418 } 419 case 1: 420 manifestApps[0].Merge(&contextApp) 421 err = addApp(&apps, manifestApps[0]) 422 default: 423 selectedAppName := contextApp.Name 424 contextApp.Name = nil 425 426 if !contextApp.IsEmpty() { 427 cmd.ui.Failed("%s", T("Incorrect Usage. Command line flags (except -f) cannot be applied when pushing multiple apps from a manifest file.")) 428 } 429 430 if selectedAppName != nil { 431 var manifestApp models.AppParams 432 manifestApp, err = findAppWithNameInManifest(*selectedAppName, manifestApps) 433 if err == nil { 434 addApp(&apps, manifestApp) 435 } 436 } else { 437 for _, manifestApp := range manifestApps { 438 addApp(&apps, manifestApp) 439 } 440 } 441 } 442 443 if err != nil { 444 cmd.ui.Failed(T("Error: {{.Err}}", map[string]interface{}{"Err": err.Error()})) 445 } 446 447 return 448 } 449 450 func addApp(apps *[]models.AppParams, app models.AppParams) (err error) { 451 if app.Name == nil { 452 err = errors.New(T("App name is a required field")) 453 } 454 if app.Path == nil { 455 cwd, _ := os.Getwd() 456 app.Path = &cwd 457 } 458 *apps = append(*apps, app) 459 return 460 } 461 462 func findAppWithNameInManifest(name string, manifestApps []models.AppParams) (app models.AppParams, err error) { 463 for _, appParams := range manifestApps { 464 if appParams.Name != nil && *appParams.Name == name { 465 app = appParams 466 return 467 } 468 } 469 470 err = errors.New(T("Could not find app named '{{.AppName}}' in manifest", 471 map[string]interface{}{"AppName": name})) 472 return 473 } 474 475 func (cmd *Push) getAppParamsFromContext(c *cli.Context) (appParams models.AppParams) { 476 if len(c.Args()) > 0 { 477 appParams.Name = &c.Args()[0] 478 } 479 480 appParams.NoRoute = c.Bool("no-route") 481 appParams.UseRandomHostname = c.Bool("random-route") 482 483 if c.String("n") != "" { 484 hostname := c.String("n") 485 appParams.Hosts = &[]string{hostname} 486 } 487 488 if c.String("b") != "" { 489 buildpack := c.String("b") 490 if buildpack == "null" || buildpack == "default" { 491 buildpack = "" 492 } 493 appParams.BuildpackUrl = &buildpack 494 } 495 496 if c.String("c") != "" { 497 command := c.String("c") 498 if command == "null" || command == "default" { 499 command = "" 500 } 501 appParams.Command = &command 502 } 503 504 if c.String("d") != "" { 505 domain := c.String("d") 506 appParams.Domain = &domain 507 } 508 509 if c.IsSet("i") { 510 instances := c.Int("i") 511 if instances < 1 { 512 cmd.ui.Failed(T("Invalid instance count: {{.InstancesCount}}\nInstance count must be a positive integer", 513 map[string]interface{}{"InstancesCount": instances})) 514 } 515 appParams.InstanceCount = &instances 516 } 517 518 if c.String("k") != "" { 519 diskQuota, err := formatters.ToMegabytes(c.String("k")) 520 if err != nil { 521 cmd.ui.Failed(T("Invalid disk quota: {{.DiskQuota}}\n{{.Err}}", 522 map[string]interface{}{"DiskQuota": c.String("k"), "Err": err.Error()})) 523 } 524 appParams.DiskQuota = &diskQuota 525 } 526 527 if c.String("m") != "" { 528 memory, err := formatters.ToMegabytes(c.String("m")) 529 if err != nil { 530 cmd.ui.Failed(T("Invalid memory limit: {{.MemLimit}}\n{{.Err}}", 531 map[string]interface{}{"MemLimit": c.String("m"), "Err": err.Error()})) 532 } 533 appParams.Memory = &memory 534 } 535 536 if c.String("p") != "" { 537 path := c.String("p") 538 appParams.Path = &path 539 } 540 541 if c.String("s") != "" { 542 stackName := c.String("s") 543 appParams.StackName = &stackName 544 } 545 546 if c.String("t") != "" { 547 timeout, err := strconv.Atoi(c.String("t")) 548 if err != nil { 549 cmd.ui.Failed("Error: %s", errors.NewWithFmt(T("Invalid timeout param: {{.Timeout}}\n{{.Err}}", 550 map[string]interface{}{"Timeout": c.String("t"), "Err": err.Error()}))) 551 } 552 553 appParams.HealthCheckTimeout = &timeout 554 } 555 556 return 557 } 558 559 func (cmd *Push) uploadApp(appGuid string, appDir string) (apiErr error) { 560 fileutils.TempDir("apps", func(uploadDir string, err error) { 561 if err != nil { 562 apiErr = err 563 return 564 } 565 566 presentFiles, err := cmd.actor.GatherFiles(appDir, uploadDir) 567 if err != nil { 568 apiErr = err 569 return 570 } 571 572 fileutils.TempFile("uploads", func(zipFile *os.File, err error) { 573 err = cmd.zipAppFiles(zipFile, appDir, uploadDir) 574 if err != nil { 575 apiErr = err 576 return 577 } 578 579 err = cmd.actor.UploadApp(appGuid, zipFile, presentFiles) 580 if err != nil { 581 apiErr = err 582 return 583 } 584 }) 585 return 586 }) 587 return 588 } 589 590 func (cmd *Push) zipAppFiles(zipFile *os.File, appDir string, uploadDir string) (zipErr error) { 591 zipErr = cmd.zipWithBetterErrors(uploadDir, zipFile) 592 if zipErr != nil { 593 return 594 } 595 596 zipFileSize, zipErr := cmd.zipper.GetZipSize(zipFile) 597 if zipErr != nil { 598 return 599 } 600 601 zipFileCount := cmd.app_files.CountFiles(uploadDir) 602 603 cmd.describeUploadOperation(appDir, zipFileSize, zipFileCount) 604 return 605 } 606 607 func (cmd *Push) zipWithBetterErrors(uploadDir string, zipFile *os.File) error { 608 zipError := cmd.zipper.Zip(uploadDir, zipFile) 609 switch err := zipError.(type) { 610 case nil: 611 return nil 612 case *errors.EmptyDirError: 613 zipFile = nil 614 return zipError 615 default: 616 return errors.NewWithError(T("Error zipping application"), err) 617 } 618 } 619 620 func (cmd *Push) describeUploadOperation(path string, zipFileBytes, fileCount int64) { 621 if fileCount > 0 { 622 cmd.ui.Say(T("Uploading app files from: {{.Path}}", map[string]interface{}{"Path": path})) 623 cmd.ui.Say(T("Uploading {{.ZipFileBytes}}, {{.FileCount}} files", 624 map[string]interface{}{ 625 "ZipFileBytes": formatters.ByteSize(zipFileBytes), 626 "FileCount": fileCount})) 627 } else { 628 cmd.ui.Warn(T("None of your application files have changed. Nothing will be uploaded.")) 629 } 630 }