11package commands
22
33import (
4+ "bytes"
45 "fmt"
56 "io"
67 "sort"
78 "strings"
89 "text/tabwriter"
910 "time"
1011
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"
1116 "github.com/docker/app/internal/store"
1217 "github.com/docker/cli/cli"
1318 "github.com/docker/cli/cli/command"
@@ -22,55 +27,79 @@ import (
2227var (
2328 listColumns = []struct {
2429 header string
25- value func (i * store. Installation ) string
30+ value func (i Installation ) string
2631 }{
27- {"RUNNING APP" , func (i * store.Installation ) string { return i .Name }},
28- {"APP NAME" , func (i * store.Installation ) string { return fmt .Sprintf ("%s (%s)" , i .Bundle .Name , i .Bundle .Version ) }},
29- {"LAST ACTION" , func (i * store.Installation ) string { return i .Result .Action }},
30- {"RESULT" , func (i * store.Installation ) string { return i .Result .Status }},
31- {"CREATED" , func (i * store.Installation ) string {
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 {
3238 return fmt .Sprintf ("%s ago" , units .HumanDuration (time .Since (i .Created )))
3339 }},
34- {"MODIFIED" , func (i * store. Installation ) string {
40+ {"MODIFIED" , func (i Installation ) string {
3541 return fmt .Sprintf ("%s ago" , units .HumanDuration (time .Since (i .Modified )))
3642 }},
37- {"REFERENCE" , func (i * store. Installation ) string { return i .Reference }},
43+ {"REFERENCE" , func (i Installation ) string { return i .Reference }},
3844 }
3945)
4046
41- func listCmd (dockerCli command.Cli ) * cobra.Command {
42- var template string
47+ type listOptions struct {
48+ template string
49+ }
50+
51+ func listCmd (dockerCli command.Cli , installerContext * cliopts.InstallerContextOptions ) * cobra.Command {
52+ var opts listOptions
4353 cmd := & cobra.Command {
4454 Use : "ls [OPTIONS]" ,
4555 Short : "List running Apps" ,
4656 Aliases : []string {"list" },
4757 Args : cli .NoArgs ,
4858 RunE : func (cmd * cobra.Command , args []string ) error {
49- return runList (dockerCli , template )
59+ return runList (dockerCli , opts , installerContext )
5060 },
5161 }
5262
53- cmd .Flags ().StringVarP (& template , "format" , "f" , "" , "Format the output using the given syntax or Go template" )
63+ cmd .Flags ().StringVarP (& opts . template , "format" , "f" , "" , "Format the output using the given syntax or Go template" )
5464 cmd .Flags ().SetAnnotation ("format" , "experimentalCLI" , []string {"true" }) //nolint:errcheck
5565 return cmd
5666}
5767
58- func runList (dockerCli command.Cli , template string ) error {
59- installations , err := getInstallations (dockerCli .CurrentContext (), config .Dir ())
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+ }
6089 if err != nil {
6190 return err
6291 }
6392
64- if template == "json" {
93+ if opts . template == "json" {
6594 bytes , err := json .MarshalIndent (installations , "" , " " )
6695 if err != nil {
6796 return errors .Errorf ("Failed to marshall json: %s" , err )
6897 }
6998 _ , err = dockerCli .Out ().Write (bytes )
7099 return err
71100 }
72- if template != "" {
73- tmpl , err := templates .Parse (template )
101+ if opts . template != "" {
102+ tmpl , err := templates .Parse (opts . template )
74103 if err != nil {
75104 return errors .Errorf ("Template parsing error: %s" , err )
76105 }
@@ -94,38 +123,128 @@ func printHeaders(w io.Writer) {
94123 fmt .Fprintln (w , strings .Join (headers , "\t " ))
95124}
96125
97- func printValues (w io.Writer , installation * store. Installation ) {
126+ func printValues (w io.Writer , installation Installation ) {
98127 var values []string
99128 for _ , column := range listColumns {
100129 values = append (values , column .value (installation ))
101130 }
102131 fmt .Fprintln (w , strings .Join (values , "\t " ))
103132}
104133
105- func getInstallations (targetContext , configDir string ) ([]* store.Installation , error ) {
106- appstore , err := store .NewApplicationStore (configDir )
107- if err != nil {
108- return nil , err
109- }
110- installationStore , err := appstore .InstallationStore (targetContext )
111- if err != nil {
112- return nil , err
113- }
134+ type Installation struct {
135+ * store.Installation
136+ Services appServices `json:",omitempty"`
137+ }
138+
139+ func getInstallations (installationStore store.InstallationStore , fetcher ServiceFetcher ) ([]Installation , error ) {
114140 installationNames , err := installationStore .List ()
115141 if err != nil {
116142 return nil , err
117143 }
118- installations := make ([]* store. Installation , len (installationNames ))
144+ installations := make ([]Installation , len (installationNames ))
119145 for i , name := range installationNames {
120146 installation , err := installationStore .Read (name )
121147 if err != nil {
122148 return nil , err
123149 }
124- installations [i ] = installation
150+ services , err := fetcher .getServices (installation )
151+ if err != nil {
152+ return nil , err
153+ }
154+ installations [i ] = Installation {Installation : installation , Services : services }
125155 }
126156 // Sort installations with last modified first
127157 sort .Slice (installations , func (i , j int ) bool {
128158 return installations [i ].Modified .After (installations [j ].Modified )
129159 })
160+
130161 return installations , nil
131162}
163+
164+ type ServiceStatus struct {
165+ DesiredTasks int
166+ RunningTasks int
167+ }
168+
169+ type appServices map [string ]ServiceStatus
170+
171+ type runningService struct {
172+ Spec struct {
173+ Name string
174+ }
175+ ServiceStatus ServiceStatus
176+ }
177+
178+ type serviceFetcher struct {
179+ dockerCli command.Cli
180+ opts listOptions
181+ installerContext * cliopts.InstallerContextOptions
182+ }
183+
184+ type ServiceFetcher interface {
185+ getServices (* store.Installation ) (appServices , error )
186+ }
187+
188+ func (s * serviceFetcher ) getServices (installation * store.Installation ) (appServices , error ) {
189+ defer muteDockerCli (s .dockerCli )()
190+
191+ // bundle without status action returns empty services
192+ if ! hasAction (installation .Bundle , internal .ActionStatusJSONName ) {
193+ return nil , nil
194+ }
195+ creds , err := prepareCredentialSet (installation .Bundle ,
196+ addDockerCredentials (s .dockerCli .CurrentContext (), s .dockerCli .ContextStore ()),
197+ addRegistryCredentials (false , s .dockerCli ),
198+ )
199+ if err != nil {
200+ return nil , err
201+ }
202+
203+ var buf bytes.Buffer
204+ driverImpl , errBuf , err := cnab .SetupDriver (installation , s .dockerCli , s .installerContext , & buf )
205+ if err != nil {
206+ return nil , err
207+ }
208+ a := & action.RunCustom {
209+ Driver : driverImpl ,
210+ Action : internal .ActionStatusJSONName ,
211+ }
212+ // fetch output from status JSON action and parse it
213+ if err := a .Run (& installation .Claim , creds ); err != nil {
214+ return nil , fmt .Errorf ("failed to get app %q status : %s\n %s" , installation .Name , err , errBuf )
215+ }
216+ var runningServices []runningService
217+ if err := json .Unmarshal (buf .Bytes (), & runningServices ); err != nil {
218+ return nil , err
219+ }
220+
221+ services := make (appServices , len (installation .Bundle .Images ))
222+ for name := range installation .Bundle .Images {
223+ services [name ] = getRunningService (runningServices , installation .Name , name )
224+ }
225+
226+ return services , nil
227+ }
228+
229+ func getRunningService (services []runningService , app , name string ) ServiceStatus {
230+ for _ , s := range services {
231+ // swarm services are prefixed by app name
232+ if s .Spec .Name == name || s .Spec .Name == fmt .Sprintf ("%s_%s" , app , name ) {
233+ return s .ServiceStatus
234+ }
235+ }
236+ return ServiceStatus {}
237+ }
238+
239+ func printServices (i Installation ) string {
240+ if len (i .Services ) == 0 {
241+ return "N/A"
242+ }
243+ var runningServices int
244+ for _ , s := range i .Services {
245+ if s .RunningTasks > 0 {
246+ runningServices ++
247+ }
248+ }
249+ return fmt .Sprintf ("%d/%d" , runningServices , len (i .Services ))
250+ }
0 commit comments