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 }