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  }