github.com/tickoalcantara12/micro/v3@v3.0.0-20221007104245-9d75b9bcbab9/client/cli/run/service.go (about) 1 // Package runtime is the micro runtime 2 package runtime 3 4 import ( 5 "encoding/json" 6 "fmt" 7 "net/http" 8 "os" 9 "os/signal" 10 "path" 11 "path/filepath" 12 "sort" 13 "strings" 14 "syscall" 15 "text/tabwriter" 16 "time" 17 18 "github.com/tickoalcantara12/micro/v3/client/cli/namespace" 19 "github.com/tickoalcantara12/micro/v3/client/cli/util" 20 "github.com/tickoalcantara12/micro/v3/service/logger" 21 "github.com/tickoalcantara12/micro/v3/service/runtime" 22 "github.com/tickoalcantara12/micro/v3/service/runtime/source/git" 23 "github.com/tickoalcantara12/micro/v3/util/config" 24 "github.com/urfave/cli/v2" 25 "golang.org/x/net/publicsuffix" 26 "google.golang.org/grpc/codes" 27 "google.golang.org/grpc/status" 28 ) 29 30 const ( 31 // RunUsage message for the run command 32 RunUsage = "Run a service: micro run [source]" 33 // KillUsage message for the kill command 34 KillUsage = "Kill a service: micro kill [source]" 35 // UpdateUsage message for the update command 36 UpdateUsage = "Update a service: micro update [source]" 37 // GetUsage message for micro get command 38 GetUsage = "Get the status of services" 39 // ServicesUsage message for micro services command 40 ServicesUsage = "micro services" 41 // CannotWatch message for the run command 42 CannotWatch = "Cannot watch filesystem on this runtime" 43 ) 44 45 var ( 46 // DefaultRetries which should be attempted when starting a service 47 DefaultRetries = 3 48 // Git orgs we currently support for credentials 49 GitOrgs = []string{"github", "bitbucket", "gitlab"} 50 httpClient = &http.Client{} 51 ) 52 53 const ( 54 credentialsKey = "GIT_CREDENTIALS" 55 ) 56 57 // timeAgo returns the time passed 58 func timeAgo(v string) string { 59 if len(v) == 0 { 60 return "unknown" 61 } 62 t, err := time.Parse(time.RFC3339, v) 63 if err != nil { 64 return v 65 } 66 67 return fmt.Sprintf("%v ago", fmtDuration(time.Since(t))) 68 } 69 70 func fmtDuration(d time.Duration) string { 71 // round to secs 72 d = d.Round(time.Second) 73 74 var resStr string 75 days := d / (time.Hour * 24) 76 if days > 0 { 77 d -= days * time.Hour * 24 78 resStr = fmt.Sprintf("%dd", days) 79 } 80 h := d / time.Hour 81 if len(resStr) > 0 || h > 0 { 82 d -= h * time.Hour 83 resStr = fmt.Sprintf("%s%dh", resStr, h) 84 } 85 m := d / time.Minute 86 if len(resStr) > 0 || m > 0 { 87 d -= m * time.Minute 88 resStr = fmt.Sprintf("%s%dm", resStr, m) 89 } 90 s := d / time.Second 91 resStr = fmt.Sprintf("%s%ds", resStr, s) 92 return resStr 93 } 94 95 // exists returns whether the given file or directory exists 96 func dirExists(path string) (bool, error) { 97 _, err := os.Stat(path) 98 if err == nil { 99 return true, nil 100 } 101 if os.IsNotExist(err) { 102 return false, nil 103 } 104 return true, err 105 } 106 107 func sourceExists(source *git.Source) error { 108 sourceExistsAt := func(url, ref string, source *git.Source) error { 109 req, _ := http.NewRequest("GET", url, nil) 110 111 // add the git credentials if set 112 if creds, ok := getGitCredentials(source.Repo); ok { 113 req.Header.Set("Authorization", "token "+creds) 114 } 115 116 resp, err := httpClient.Do(req) 117 118 // @todo gracefully degrade? 119 if err != nil { 120 return err 121 } 122 // if the client was rate-limited, fall back to assuming the service url is valid 123 if resp.StatusCode == 403 { 124 return nil 125 } 126 if resp.StatusCode >= 400 && resp.StatusCode < 500 { 127 return fmt.Errorf("service at %v@%v not found", source.RuntimeSource(), ref) 128 } 129 return nil 130 } 131 132 doSourceExists := func(ref string) error { 133 if strings.HasPrefix(source.Repo, "github.com") { 134 // Github specific existence checks 135 repo := strings.ReplaceAll(source.Repo, "github.com/", "") 136 url := fmt.Sprintf("https://api.github.com/repos/%v/contents/%v?ref=%v", repo, source.Folder, ref) 137 return sourceExistsAt(url, ref, source) 138 } else if strings.HasPrefix(source.Repo, "gitlab.com") { 139 // Gitlab specific existence checks 140 141 // @todo better check for gitlab 142 url := fmt.Sprintf("https://%v", source.Repo) 143 return sourceExistsAt(url, ref, source) 144 } 145 return nil 146 } 147 148 ref := source.Ref 149 if ref != "latest" && ref != "" { 150 return doSourceExists(ref) 151 } 152 defaults := []string{"latest", "master", "main", "trunk"} 153 var ret error 154 for _, ref := range defaults { 155 ret = doSourceExists(ref) 156 if ret == nil { 157 return nil 158 } 159 } 160 return ret 161 162 } 163 164 // try to find a matching source 165 // returns true if found 166 func getMatchingSource(nameOrSource string) (string, bool) { 167 services, err := runtime.Read() 168 if err == nil { 169 for _, service := range services { 170 parts := strings.Split(nameOrSource, "@") 171 if len(parts) > 1 && service.Name == parts[0] && service.Version == parts[1] { 172 return service.Metadata["source"], true 173 } 174 175 if len(parts) == 1 && service.Name == nameOrSource { 176 return service.Metadata["source"], true 177 } 178 } 179 } 180 return "", false 181 } 182 183 // matchExistingService true: load running services and expand the shortname of a service 184 // ie micro update invite becomes micro update github.com/m3o/services/invite 185 func appendSourceBase(ctx *cli.Context, workDir, source string, matchExistingService bool) string { 186 isLocal, _ := git.IsLocal(workDir, source) 187 // @todo add list of supported hosts here or do this check better 188 domain := strings.Split(source, "/")[0] 189 _, err := publicsuffix.EffectiveTLDPlusOne(domain) 190 if !isLocal && err != nil { 191 // read the service. In case there is an existing service with the same name and version 192 // use its source 193 if matchExistingService { 194 matchedSource, hasMatching := getMatchingSource(source) 195 if hasMatching { 196 return matchedSource 197 } 198 } 199 200 env, _ := util.GetEnv(ctx) 201 202 baseURL, _ := config.Get(config.Path("git", env.Name, "baseurl")) 203 if len(baseURL) == 0 { 204 baseURL, _ = config.Get(config.Path("git", "baseurl")) 205 } 206 if len(baseURL) == 0 { 207 return path.Join("github.com/micro/services", source) 208 } 209 return path.Join(baseURL, source) 210 } 211 return source 212 } 213 214 // watchService watches the changes of source directory, rebuild and restart the service 215 func watchService(ctx *cli.Context, source *git.Source, srv *runtime.Service, opts []runtime.CreateOption) error { 216 // always force rebuild the service 217 opts = append(opts, runtime.WithForce(true)) 218 219 watchDelay := time.Duration(ctx.Int("watch_delay")) * time.Millisecond 220 watcher, err := NewWatcher(source.FullPath, watchDelay, func() error { 221 logger.Infof("Watching process: rebuilding...") 222 223 // upload the service source again 224 _, err := upload(ctx, srv, source) 225 if err != nil { 226 logger.Errorf("Watching process: upload error: %v", err) 227 return err 228 } 229 230 // restart the service 231 if err := runtime.Create(srv, opts...); err != nil { 232 logger.Errorf("Watching process: create service error: %v", err) 233 return err 234 } 235 236 logger.Info("Watching process: build success") 237 return nil 238 }) 239 240 if err != nil { 241 return nil 242 } 243 244 // gracefully exit 245 sigs := make(chan os.Signal, 1) 246 signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM) 247 248 go func() { 249 <-sigs 250 watcher.Stop() 251 }() 252 253 // start watching 254 err = watcher.Watch() 255 if err != nil { 256 return err 257 } 258 259 return nil 260 } 261 262 func runService(ctx *cli.Context) error { 263 // we need some args to run 264 if ctx.Args().Len() == 0 { 265 return cli.ShowSubcommandHelp(ctx) 266 } 267 268 wd, err := os.Getwd() 269 if err != nil { 270 return err 271 } 272 273 // determine the type of source input, i.e. is it a local folder or a remote git repo 274 source, err := git.ParseSourceLocal(wd, appendSourceBase(ctx, wd, ctx.Args().Get(0), false)) 275 if err != nil { 276 return err 277 } 278 279 // if the source isn't local, ensure it exists 280 if !source.Local { 281 if err := sourceExists(source); err != nil { 282 return err 283 } 284 } 285 286 // get name from flag 287 name := ctx.String("name") 288 if len(name) == 0 { 289 name = source.RuntimeName() 290 } 291 292 // parse the various flags 293 typ := ctx.String("type") 294 command := strings.TrimSpace(ctx.String("command")) 295 args := strings.TrimSpace(ctx.String("args")) 296 retries := DefaultRetries 297 var image string 298 var instances int 299 300 if ctx.IsSet("retries") { 301 retries = ctx.Int("retries") 302 } 303 if ctx.IsSet("image") { 304 image = ctx.String("image") 305 } 306 if ctx.IsSet("instances") { 307 instances = ctx.Int("instances") 308 } 309 310 // construct the service 311 srv := &runtime.Service{ 312 Name: name, 313 Version: source.Ref, 314 } 315 316 if source.Local { 317 // check to see if a vendor folder exists, if it doesn't we should delete the one we generate 318 // after we finish the upload 319 vendorDir := filepath.Join(source.LocalRepoRoot, "vendor") 320 if _, err := os.Stat(vendorDir); os.IsNotExist(err) { 321 defer os.RemoveAll(vendorDir) 322 } else if err != nil { 323 return err 324 } 325 326 // vendor the dependencies 327 if err := vendorDependencies(source.LocalRepoRoot); err != nil { 328 return err 329 } 330 331 // for local source, upload it to the server and use the resulting source ID 332 srv.Source, err = upload(ctx, srv, source) 333 if err != nil { 334 return err 335 } 336 } else { 337 // if we're running a remote git repository, pass this as the source 338 srv.Source = source.RuntimeSource() 339 } 340 341 // for local source, the srv.Source attribute will be remapped to the id of the source upload. 342 // however this won't make sense from a user experience perspective, so we'll pass the argument 343 // they used in metadata, e.g. ./helloworld 344 srv.Metadata = map[string]string{ 345 "source": source.RuntimeSource(), 346 } 347 348 if md := ctx.StringSlice("metadata"); len(md) > 0 { 349 for _, val := range md { 350 split := strings.Split(val, "=") 351 if len(split) != 2 { 352 return fmt.Errorf("invalid metadata string, must be of form foo=bar %s", val) 353 } 354 if split[0] == "source" { 355 // reserved 356 return fmt.Errorf("invalid metadata string, 'source' is a reserved key") 357 } 358 359 srv.Metadata[split[0]] = split[1] 360 } 361 } 362 363 // specify the options 364 opts := []runtime.CreateOption{ 365 runtime.WithOutput(os.Stdout), 366 runtime.WithRetries(retries), 367 runtime.CreateImage(image), 368 runtime.CreateType(typ), 369 runtime.WithForce(ctx.Bool("force")), 370 } 371 if instances > 0 { 372 opts = append(opts, runtime.CreateInstances(instances)) 373 } 374 if len(command) > 0 { 375 opts = append(opts, runtime.WithCommand(strings.Split(command, " ")...)) 376 } 377 if len(args) > 0 { 378 opts = append(opts, runtime.WithArgs(strings.Split(args, " ")...)) 379 } 380 381 // when the repo root doesn't match the full path (e.g. in cases where a mono-repo is being 382 // used), find the relative path and pass this in the metadata as entrypoint. 383 if source.Local && source.LocalRepoRoot != source.FullPath { 384 ep, _ := filepath.Rel(source.LocalRepoRoot, source.FullPath) 385 opts = append(opts, runtime.CreateEntrypoint(ep)) 386 } 387 388 // add environment variable passed in via cli 389 var environment []string 390 for _, evar := range ctx.StringSlice("env_vars") { 391 for _, e := range strings.Split(evar, ",") { 392 if len(e) > 0 { 393 environment = append(environment, strings.TrimSpace(e)) 394 } 395 } 396 } 397 if len(environment) > 0 { 398 opts = append(opts, runtime.WithEnv(environment)) 399 } 400 if len(command) > 0 { 401 opts = append(opts, runtime.WithCommand(strings.Split(command, " ")...)) 402 } 403 404 if len(args) > 0 { 405 opts = append(opts, runtime.WithArgs(strings.Split(args, " ")...)) 406 } 407 408 // determine the namespace 409 env, err := util.GetEnv(ctx) 410 if err != nil { 411 return err 412 } 413 ns, err := namespace.Get(env.Name) 414 if err != nil { 415 return err 416 } 417 418 opts = append(opts, runtime.CreateNamespace(ns)) 419 gitCreds, ok := getGitCredentials(source.Repo) 420 if ok { 421 opts = append(opts, runtime.WithSecret(credentialsKey, gitCreds)) 422 } 423 424 // run the service 425 err = runtime.Create(srv, opts...) 426 427 if source.Local && ctx.Bool("watch") { 428 if err := watchService(ctx, source, srv, opts); err != nil { 429 return util.CliError(err) 430 } 431 } 432 433 return util.CliError(err) 434 } 435 436 func getGitCredentials(repo string) (string, bool) { 437 repo = strings.Split(repo, "/")[0] 438 439 for _, org := range GitOrgs { 440 if !strings.Contains(repo, org) { 441 continue 442 } 443 444 // check the creds for the org 445 creds, err := config.Get(config.Path("git", "credentials", org)) 446 if err == nil && len(creds) > 0 { 447 return creds, true 448 } 449 } 450 if credURL, err := config.Get(config.Path("git", "credentials", "url")); err == nil && len(credURL) > 0 { 451 if strings.Contains(repo, credURL) { 452 creds, err := config.Get(config.Path("git", "credentials", "token")) 453 if err == nil && len(creds) > 0 { 454 return creds, true 455 } 456 } 457 } 458 459 return "", false 460 } 461 462 func killService(ctx *cli.Context) error { 463 // we need some args to run 464 if ctx.Args().Len() == 0 { 465 return cli.ShowSubcommandHelp(ctx) 466 } 467 468 // get name from flag 469 name := ctx.String("name") 470 471 if v := ctx.Args().Get(0); len(v) > 0 { 472 name = v 473 } 474 475 // special case 476 if name == "." { 477 dir, _ := os.Getwd() 478 name = filepath.Base(dir) 479 } 480 481 var ref string 482 if parts := strings.Split(name, "@"); len(parts) > 1 { 483 name = parts[0] 484 ref = parts[1] 485 } 486 if ref == "" { 487 ref = "latest" 488 } 489 service := &runtime.Service{ 490 Name: name, 491 Version: ref, 492 } 493 494 // determine the namespace 495 env, err := util.GetEnv(ctx) 496 if err != nil { 497 return err 498 } 499 ns, err := namespace.Get(env.Name) 500 if err != nil { 501 return err 502 } 503 504 err = runtime.Delete(service, runtime.DeleteNamespace(ns)) 505 return util.CliError(err) 506 } 507 508 func updateService(ctx *cli.Context) error { 509 // we need some args to run 510 if ctx.Args().Len() == 0 { 511 return cli.ShowSubcommandHelp(ctx) 512 } 513 514 wd, err := os.Getwd() 515 if err != nil { 516 return err 517 } 518 519 // determine the type of source input, i.e. is it a local folder or a remote git repo 520 source, err := git.ParseSourceLocal(wd, appendSourceBase(ctx, wd, ctx.Args().First(), true)) 521 if err != nil { 522 return err 523 } 524 525 name := ctx.String("name") 526 527 if len(name) == 0 { 528 name = source.RuntimeName() 529 } 530 531 var ref string 532 533 if parts := strings.Split(name, "@"); len(parts) > 1 { 534 name = parts[0] 535 ref = parts[1] 536 } 537 538 // set source ref 539 if len(ref) == 0 && len(source.Ref) > 0 { 540 ref = source.Ref 541 } else if len(ref) == 0 { 542 ref = "latest" 543 } 544 545 srv := &runtime.Service{ 546 Name: name, 547 Version: ref, 548 } 549 550 if source.Local { 551 // check to see if a vendor folder exists, if it doesn't we should delete the one we generate 552 // after we finish the upload 553 vendorDir := filepath.Join(source.LocalRepoRoot, "vendor") 554 if _, err := os.Stat(vendorDir); os.IsNotExist(err) { 555 defer os.RemoveAll(vendorDir) 556 } else if err != nil { 557 return err 558 } 559 560 // vendor the dependencies 561 if err := vendorDependencies(source.LocalRepoRoot); err != nil { 562 return err 563 } 564 565 // for local source, upload it to the server and use the resulting source ID 566 srv.Source, err = upload(ctx, srv, source) 567 if err != nil { 568 return err 569 } 570 } else { 571 // if we're running a remote git repository, pass this as the source 572 srv.Source = source.RuntimeSource() 573 } 574 575 // for local source, the srv.Source attribute will be remapped to the id of the source upload. 576 // however this won't make sense from a user experience perspective, so we'll pass the argument 577 // they used in metadata, e.g. ./helloworld 578 srv.Metadata = map[string]string{ 579 "source": source.RuntimeSource(), 580 } 581 582 // when the repo root doesn't match the full path (e.g. in cases where a mono-repo is being 583 // used), find the relative path and pass this in the metadata as entrypoint 584 var opts []runtime.UpdateOption 585 if source.Local && source.LocalRepoRoot != source.FullPath { 586 ep, _ := filepath.Rel(source.LocalRepoRoot, source.FullPath) 587 opts = append(opts, runtime.UpdateEntrypoint(ep)) 588 } 589 590 // determine the namespace 591 env, err := util.GetEnv(ctx) 592 if err != nil { 593 return err 594 } 595 ns, err := namespace.Get(env.Name) 596 if err != nil { 597 return err 598 } 599 opts = append(opts, runtime.UpdateNamespace(ns)) 600 601 // get number of instances to run 602 if ctx.IsSet("instances") { 603 opts = append(opts, runtime.UpdateInstances(ctx.Int("instances"))) 604 } 605 606 // pass git credentials incase a private repo needs to be pulled 607 gitCreds, ok := getGitCredentials(source.Repo) 608 if ok { 609 opts = append(opts, runtime.UpdateSecret(credentialsKey, gitCreds)) 610 } 611 612 err = runtime.Update(srv, opts...) 613 return util.CliError(err) 614 } 615 616 func getService(ctx *cli.Context) error { 617 name := ctx.String("name") 618 version := "latest" 619 typ := ctx.String("type") 620 621 if ctx.Args().Len() > 0 { 622 wd, err := os.Getwd() 623 if err != nil { 624 return err 625 } 626 source, err := git.ParseSourceLocal(wd, ctx.Args().Get(0)) 627 if err != nil { 628 return err 629 } 630 name = source.RuntimeName() 631 } 632 // set version as second arg 633 if ctx.Args().Len() > 1 { 634 version = ctx.Args().Get(1) 635 } 636 637 // should we list sevices 638 var list bool 639 640 // zero args so list all 641 if ctx.Args().Len() == 0 { 642 list = true 643 } 644 645 var services []*runtime.Service 646 var readOpts []runtime.ReadOption 647 648 // return a list of services 649 switch list { 650 case true: 651 // return specific type listing 652 if len(typ) > 0 { 653 readOpts = append(readOpts, runtime.ReadType(typ)) 654 } 655 // return one service 656 default: 657 // check if service name was passed in 658 if len(name) == 0 { 659 fmt.Println(GetUsage) 660 return nil 661 } 662 663 // get service with name and version 664 readOpts = []runtime.ReadOption{ 665 runtime.ReadService(name), 666 runtime.ReadVersion(version), 667 } 668 669 // return the runtime services 670 if len(typ) > 0 { 671 readOpts = append(readOpts, runtime.ReadType(typ)) 672 } 673 } 674 675 // determine the namespace 676 env, err := util.GetEnv(ctx) 677 if err != nil { 678 return err 679 } 680 681 ns, err := namespace.Get(env.Name) 682 if err != nil { 683 return err 684 } 685 686 readOpts = append(readOpts, runtime.ReadNamespace(ns)) 687 688 // read the service 689 services, err = runtime.Read(readOpts...) 690 if err != nil { 691 return util.CliError(err) 692 } 693 694 // make sure we return UNKNOWN when empty string is supplied 695 parse := func(m string) string { 696 if len(m) == 0 { 697 return "n/a" 698 } 699 return m 700 } 701 702 // don't do anything if there's no services 703 if len(services) == 0 { 704 return nil 705 } 706 707 sort.Slice(services, func(i, j int) bool { return services[i].Name < services[j].Name }) 708 709 writer := tabwriter.NewWriter(os.Stdout, 0, 8, 1, '\t', tabwriter.AlignRight) 710 fmt.Fprintln(writer, "NAME\tVERSION\tSOURCE\tSTATUS\tBUILD\tUPDATED\tMETADATA") 711 712 for _, service := range services { 713 // cut the commit down to first 7 characters 714 build := parse(service.Metadata["build"]) 715 if len(build) > 7 { 716 build = build[:7] 717 } 718 719 // if there is an error, display this in metadata (there is no error field) 720 metadata := fmt.Sprintf("owner=%s, group=%s", parse(service.Metadata["owner"]), parse(service.Metadata["group"])) 721 if service.Status == runtime.Error { 722 metadata = fmt.Sprintf("%v, error=%v", metadata, parse(service.Metadata["error"])) 723 } 724 725 // parse when the service was started 726 updated := parse(timeAgo(service.Metadata["started"])) 727 728 // sometimes the services's source can be remapped to the build id etc, however the original 729 // argument passed to micro run is always kept in the source attribute of service metadata 730 if src, ok := service.Metadata["source"]; ok { 731 service.Source = src 732 } 733 734 fmt.Fprintf(writer, "%s\t%s\t%s\t%s\t%s\t%s\t%s\n", 735 service.Name, 736 parse(service.Version), 737 parse(service.Source), 738 humanizeStatus(service.Status), 739 build, 740 updated, 741 metadata) 742 } 743 744 writer.Flush() 745 return nil 746 } 747 748 const ( 749 // logUsage message for logs command 750 logUsage = "Required usage: micro log example" 751 ) 752 753 func getLogs(ctx *cli.Context) error { 754 logger.DefaultLogger.Init(logger.WithFields(map[string]interface{}{"service": "runtime"})) 755 if ctx.Args().Len() == 0 { 756 return cli.ShowSubcommandHelp(ctx) 757 } 758 759 name := ctx.String("name") 760 761 // set name based on input arg if specified 762 if v := ctx.Args().Get(0); len(v) > 0 { 763 name = v 764 } 765 766 // must specify service name 767 if len(name) == 0 { 768 fmt.Println(logUsage) 769 return nil 770 } 771 772 // get the args 773 options := []runtime.LogsOption{} 774 775 count := ctx.Int("lines") 776 if count > 0 { 777 options = append(options, runtime.LogsCount(int64(count))) 778 } else { 779 options = append(options, runtime.LogsCount(int64(15))) 780 } 781 782 follow := ctx.Bool("follow") 783 784 if follow { 785 options = append(options, runtime.LogsStream(follow)) 786 } 787 788 // @todo reintroduce since 789 //since := ctx.String("since") 790 //var readSince time.Time 791 //d, err := time.ParseDuration(since) 792 //if err == nil { 793 // readSince = time.Now().Add(-d) 794 //} 795 796 var ref string 797 798 if parts := strings.Split(name, "@"); len(parts) > 1 { 799 name = parts[0] 800 ref = parts[1] 801 } 802 803 // set source ref 804 if len(ref) == 0 { 805 ref = "latest" 806 } 807 808 srv := &runtime.Service{ 809 Name: name, 810 Version: ref, 811 } 812 813 // determine the namespace 814 env, err := util.GetEnv(ctx) 815 if err != nil { 816 return err 817 } 818 ns, err := namespace.Get(env.Name) 819 if err != nil { 820 return err 821 } 822 options = append(options, runtime.LogsNamespace(ns)) 823 824 logs, err := runtime.Logs(srv, options...) 825 826 if err != nil { 827 return util.CliError(err) 828 } 829 830 output := ctx.String("output") 831 832 // range over all records until its closed 833 for record := range logs.Chan() { 834 switch output { 835 case "json": 836 b, _ := json.Marshal(record) 837 fmt.Printf("%v\n", string(b)) 838 default: 839 fmt.Printf("%v\n", record.Message) 840 } 841 } 842 843 // check for an error 844 if err := logs.Error(); err != nil { 845 if status.Convert(err).Code() == codes.NotFound { 846 return cli.Exit("Service not found", 1) 847 } 848 return util.CliError(fmt.Errorf("Error reading logs: %s\n", status.Convert(err).Message())) 849 } 850 851 return nil 852 } 853 854 func humanizeStatus(status runtime.ServiceStatus) string { 855 switch status { 856 case runtime.Pending: 857 return "pending" 858 case runtime.Building: 859 return "building" 860 case runtime.Starting: 861 return "starting" 862 case runtime.Running: 863 return "running" 864 case runtime.Stopping: 865 return "stopping" 866 case runtime.Stopped: 867 return "stopped" 868 case runtime.Error: 869 return "error" 870 default: 871 return "unknown" 872 } 873 }