github.com/docker/app@v0.9.1-beta3.0.20210611140623-a48f773ab002/internal/commands/list.go (about)

     1  package commands
     2  
     3  import (
     4  	"bytes"
     5  	"fmt"
     6  	"io"
     7  	"sort"
     8  	"strings"
     9  	"text/tabwriter"
    10  	"time"
    11  
    12  	"github.com/deislabs/cnab-go/action"
    13  	"github.com/docker/app/internal"
    14  	"github.com/docker/app/internal/cliopts"
    15  	"github.com/docker/app/internal/cnab"
    16  	"github.com/docker/app/internal/store"
    17  	"github.com/docker/cli/cli"
    18  	"github.com/docker/cli/cli/command"
    19  	"github.com/docker/cli/cli/config"
    20  	"github.com/docker/cli/templates"
    21  	units "github.com/docker/go-units"
    22  	"github.com/docker/go/canonical/json"
    23  	"github.com/pkg/errors"
    24  	"github.com/spf13/cobra"
    25  )
    26  
    27  var (
    28  	listColumns = []struct {
    29  		header string
    30  		value  func(i Installation) string
    31  	}{
    32  		{"RUNNING APP", func(i Installation) string { return i.Name }},
    33  		{"APP NAME", func(i Installation) string { return fmt.Sprintf("%s (%s)", i.Bundle.Name, i.Bundle.Version) }},
    34  		{"SERVICES", printServices},
    35  		{"LAST ACTION", func(i Installation) string { return i.Result.Action }},
    36  		{"RESULT", func(i Installation) string { return i.Result.Status }},
    37  		{"CREATED", func(i Installation) string {
    38  			return fmt.Sprintf("%s ago", units.HumanDuration(time.Since(i.Created)))
    39  		}},
    40  		{"MODIFIED", func(i Installation) string {
    41  			return fmt.Sprintf("%s ago", units.HumanDuration(time.Since(i.Modified)))
    42  		}},
    43  		{"REFERENCE", func(i Installation) string { return i.Reference }},
    44  	}
    45  )
    46  
    47  type listOptions struct {
    48  	template string
    49  }
    50  
    51  func listCmd(dockerCli command.Cli, installerContext *cliopts.InstallerContextOptions) *cobra.Command {
    52  	var opts listOptions
    53  	cmd := &cobra.Command{
    54  		Use:     "ls [OPTIONS]",
    55  		Short:   "List running Apps",
    56  		Aliases: []string{"list"},
    57  		Args:    cli.NoArgs,
    58  		RunE: func(cmd *cobra.Command, args []string) error {
    59  			return runList(dockerCli, opts, installerContext)
    60  		},
    61  	}
    62  
    63  	cmd.Flags().StringVarP(&opts.template, "format", "f", "", "Format the output using the given syntax or Go template")
    64  	cmd.Flags().SetAnnotation("format", "experimentalCLI", []string{"true"}) //nolint:errcheck
    65  	return cmd
    66  }
    67  
    68  func runList(dockerCli command.Cli, opts listOptions, installerContext *cliopts.InstallerContextOptions) error {
    69  	// initialize stores
    70  	appstore, err := store.NewApplicationStore(config.Dir())
    71  	if err != nil {
    72  		return err
    73  	}
    74  	targetContext := dockerCli.CurrentContext()
    75  	installationStore, err := appstore.InstallationStore(targetContext)
    76  	if err != nil {
    77  		return err
    78  	}
    79  
    80  	fetcher := &serviceFetcher{
    81  		dockerCli:        dockerCli,
    82  		opts:             opts,
    83  		installerContext: installerContext,
    84  	}
    85  	installations, err := getInstallations(installationStore, fetcher)
    86  	if installations == nil && err != nil {
    87  		return err
    88  	}
    89  	if err != nil {
    90  		fmt.Fprintf(dockerCli.Err(), "%s\n", err)
    91  	}
    92  
    93  	if opts.template == "json" {
    94  		bytes, err := json.MarshalIndent(installations, "", "  ")
    95  		if err != nil {
    96  			return errors.Errorf("Failed to marshall json: %s", err)
    97  		}
    98  		_, err = dockerCli.Out().Write(bytes)
    99  		return err
   100  	}
   101  	if opts.template != "" {
   102  		tmpl, err := templates.Parse(opts.template)
   103  		if err != nil {
   104  			return errors.Errorf("Template parsing error: %s", err)
   105  		}
   106  		return tmpl.Execute(dockerCli.Out(), installations)
   107  	}
   108  
   109  	w := tabwriter.NewWriter(dockerCli.Out(), 20, 1, 3, ' ', 0)
   110  	printHeaders(w)
   111  
   112  	for _, installation := range installations {
   113  		printValues(w, installation)
   114  	}
   115  	return w.Flush()
   116  }
   117  
   118  func printHeaders(w io.Writer) {
   119  	var headers []string
   120  	for _, column := range listColumns {
   121  		headers = append(headers, column.header)
   122  	}
   123  	fmt.Fprintln(w, strings.Join(headers, "\t"))
   124  }
   125  
   126  func printValues(w io.Writer, installation Installation) {
   127  	var values []string
   128  	for _, column := range listColumns {
   129  		values = append(values, column.value(installation))
   130  	}
   131  	fmt.Fprintln(w, strings.Join(values, "\t"))
   132  }
   133  
   134  type Installation struct {
   135  	*store.Installation
   136  	Services appServices `json:",omitempty"`
   137  }
   138  
   139  func getInstallations(installationStore store.InstallationStore, fetcher ServiceFetcher) ([]Installation, error) {
   140  	installationNames, err := installationStore.List()
   141  	if err != nil {
   142  		return nil, err
   143  	}
   144  	installations := make([]Installation, len(installationNames))
   145  	var errs []string
   146  	for i, name := range installationNames {
   147  		installation, err := installationStore.Read(name)
   148  		if err != nil {
   149  			return nil, err
   150  		}
   151  		services, err := fetcher.getServices(installation)
   152  		if err != nil {
   153  			errs = append(errs, err.Error())
   154  		}
   155  		installations[i] = Installation{Installation: installation, Services: services}
   156  	}
   157  	// Sort installations with last modified first
   158  	sort.Slice(installations, func(i, j int) bool {
   159  		return installations[i].Modified.After(installations[j].Modified)
   160  	})
   161  	if len(errs) > 0 {
   162  		err = errors.New(strings.Join(errs, "\n"))
   163  	}
   164  	return installations, err
   165  }
   166  
   167  type ServiceStatus struct {
   168  	DesiredTasks int
   169  	RunningTasks int
   170  }
   171  
   172  type appServices map[string]ServiceStatus
   173  
   174  type runningService struct {
   175  	Spec struct {
   176  		Name string
   177  	}
   178  	ServiceStatus ServiceStatus
   179  }
   180  
   181  type serviceFetcher struct {
   182  	dockerCli        command.Cli
   183  	opts             listOptions
   184  	installerContext *cliopts.InstallerContextOptions
   185  }
   186  
   187  type ServiceFetcher interface {
   188  	getServices(*store.Installation) (appServices, error)
   189  }
   190  
   191  func (s *serviceFetcher) getServices(installation *store.Installation) (appServices, error) {
   192  	defer muteDockerCli(s.dockerCli)()
   193  
   194  	// bundle without status action returns empty services
   195  	if !hasAction(installation.Bundle, internal.ActionStatusJSONName) {
   196  		return nil, nil
   197  	}
   198  	creds, err := prepareCredentialSet(installation.Bundle,
   199  		addDockerCredentials(s.dockerCli.CurrentContext(), s.dockerCli.ContextStore()),
   200  		addRegistryCredentials(false, s.dockerCli),
   201  	)
   202  	if err != nil {
   203  		return nil, err
   204  	}
   205  
   206  	var buf bytes.Buffer
   207  	driverImpl, errBuf, err := cnab.SetupDriver(installation, s.dockerCli, s.installerContext, &buf)
   208  	if err != nil {
   209  		return nil, err
   210  	}
   211  	a := &action.RunCustom{
   212  		Driver: driverImpl,
   213  		Action: internal.ActionStatusJSONName,
   214  	}
   215  	// fetch output from status JSON action and parse it
   216  	if err := a.Run(&installation.Claim, creds); err != nil {
   217  		return nil, fmt.Errorf("failed to get app %q status : %s\n%s", installation.Name, err, errBuf)
   218  	}
   219  	var runningServices []runningService
   220  	if err := json.Unmarshal(buf.Bytes(), &runningServices); err != nil {
   221  		return nil, err
   222  	}
   223  
   224  	services := make(appServices, len(installation.Bundle.Images))
   225  	for name := range installation.Bundle.Images {
   226  		services[name] = getRunningService(runningServices, installation.Name, name)
   227  	}
   228  
   229  	return services, nil
   230  }
   231  
   232  func getRunningService(services []runningService, app, name string) ServiceStatus {
   233  	for _, s := range services {
   234  		// swarm services are prefixed by app name
   235  		if s.Spec.Name == name || s.Spec.Name == fmt.Sprintf("%s_%s", app, name) {
   236  			return s.ServiceStatus
   237  		}
   238  	}
   239  	return ServiceStatus{}
   240  }
   241  
   242  func printServices(i Installation) string {
   243  	if len(i.Services) == 0 {
   244  		return "N/A"
   245  	}
   246  	var runningServices int
   247  	for _, s := range i.Services {
   248  		if s.RunningTasks > 0 {
   249  			runningServices++
   250  		}
   251  	}
   252  	return fmt.Sprintf("%d/%d", runningServices, len(i.Services))
   253  }