Skip to content
This repository was archived by the owner on Jul 18, 2025. It is now read-only.

Commit d230ee7

Browse files
author
Jean-Christophe Sirot
committed
Add table formating in CNAB status action
Signed-off-by: Jean-Christophe Sirot <[email protected]>
1 parent 240f7a8 commit d230ee7

File tree

3 files changed

+258
-109
lines changed

3 files changed

+258
-109
lines changed

cmd/cnab-run/status.go

Lines changed: 147 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,24 +3,144 @@ package main
33
import (
44
"encoding/json"
55
"fmt"
6+
"io"
67
"os"
8+
"sort"
9+
"strings"
10+
"text/tabwriter"
711

812
"github.com/docker/app/internal"
913
"github.com/docker/cli/cli/command"
1014
"github.com/docker/cli/cli/command/stack"
1115
"github.com/docker/cli/cli/command/stack/options"
1216
"github.com/docker/cli/opts"
17+
"github.com/docker/distribution/reference"
1318
swarmtypes "github.com/docker/docker/api/types/swarm"
19+
"github.com/docker/docker/pkg/stringid"
1420
"github.com/pkg/errors"
1521
)
1622

23+
var (
24+
listColumns = []struct {
25+
header string
26+
value func(s *swarmtypes.Service) string
27+
}{
28+
{"ID", func(s *swarmtypes.Service) string { return stringid.TruncateID(s.ID) }},
29+
{"NAME", func(s *swarmtypes.Service) string { return s.Spec.Name }},
30+
{"MODE", func(s *swarmtypes.Service) string {
31+
if s.Spec.Mode.Replicated != nil {
32+
return "replicated"
33+
}
34+
return "global"
35+
}},
36+
{"REPLICAS", func(s *swarmtypes.Service) string {
37+
if s.Spec.Mode.Replicated != nil {
38+
return fmt.Sprintf("%d/%d", s.ServiceStatus.RunningTasks, s.ServiceStatus.DesiredTasks)
39+
}
40+
return ""
41+
}},
42+
{"IMAGE", func(s *swarmtypes.Service) string {
43+
ref, err := reference.ParseAnyReference(s.Spec.TaskTemplate.ContainerSpec.Image)
44+
if err != nil {
45+
return "N/A"
46+
}
47+
if namedRef, ok := ref.(reference.Named); ok {
48+
return reference.FamiliarName(namedRef)
49+
}
50+
return reference.FamiliarString(ref)
51+
}},
52+
{"PORTS", func(s *swarmtypes.Service) string {
53+
return Ports(s.Endpoint.Ports)
54+
}},
55+
}
56+
)
57+
58+
type portRange struct {
59+
pStart uint32
60+
pEnd uint32
61+
tStart uint32
62+
tEnd uint32
63+
protocol swarmtypes.PortConfigProtocol
64+
}
65+
66+
func (pr portRange) String() string {
67+
var (
68+
pub string
69+
tgt string
70+
)
71+
72+
if pr.pEnd > pr.pStart {
73+
pub = fmt.Sprintf("%d-%d", pr.pStart, pr.pEnd)
74+
} else {
75+
pub = fmt.Sprintf("%d", pr.pStart)
76+
}
77+
if pr.tEnd > pr.tStart {
78+
tgt = fmt.Sprintf("%d-%d", pr.tStart, pr.tEnd)
79+
} else {
80+
tgt = fmt.Sprintf("%d", pr.tStart)
81+
}
82+
return fmt.Sprintf("*:%s->%s/%s", pub, tgt, pr.protocol)
83+
}
84+
85+
// Ports formats port configuration. This function is copied et adapted from docker CLI
86+
// see https://github.com/docker/cli/blob/d6edc912ce/cli/command/service/formatter.go#L655
87+
func Ports(servicePorts []swarmtypes.PortConfig) string {
88+
if servicePorts == nil {
89+
return ""
90+
}
91+
92+
pr := portRange{}
93+
ports := []string{}
94+
95+
sort.Slice(servicePorts, func(i, j int) bool {
96+
if servicePorts[i].Protocol == servicePorts[j].Protocol {
97+
return servicePorts[i].PublishedPort < servicePorts[j].PublishedPort
98+
}
99+
return servicePorts[i].Protocol < servicePorts[j].Protocol
100+
})
101+
102+
for _, p := range servicePorts {
103+
if p.PublishMode == swarmtypes.PortConfigPublishModeIngress {
104+
prIsRange := pr.tEnd != pr.tStart
105+
tOverlaps := p.TargetPort <= pr.tEnd
106+
107+
// Start a new port-range if:
108+
// - the protocol is different from the current port-range
109+
// - published or target port are not consecutive to the current port-range
110+
// - the current port-range is a _range_, and the target port overlaps with the current range's target-ports
111+
if p.Protocol != pr.protocol || p.PublishedPort-pr.pEnd > 1 || p.TargetPort-pr.tEnd > 1 || prIsRange && tOverlaps {
112+
// start a new port-range, and print the previous port-range (if any)
113+
if pr.pStart > 0 {
114+
ports = append(ports, pr.String())
115+
}
116+
pr = portRange{
117+
pStart: p.PublishedPort,
118+
pEnd: p.PublishedPort,
119+
tStart: p.TargetPort,
120+
tEnd: p.TargetPort,
121+
protocol: p.Protocol,
122+
}
123+
continue
124+
}
125+
pr.pEnd = p.PublishedPort
126+
pr.tEnd = p.TargetPort
127+
}
128+
}
129+
if pr.pStart > 0 {
130+
ports = append(ports, pr.String())
131+
}
132+
return strings.Join(ports, ", ")
133+
}
134+
17135
func statusAction(instanceName string) error {
18136
cli, err := getCli()
19137
if err != nil {
20138
return err
21139
}
22140
services, _ := runningServices(cli, instanceName)
23-
fmt.Fprintln(cli.Out(), services)
141+
if err := printServices(cli.Out(), services); err != nil {
142+
return err
143+
}
24144
return nil
25145
}
26146

@@ -57,3 +177,29 @@ func runningServices(cli command.Cli, instanceName string) ([]swarmtypes.Service
57177
Namespace: instanceName,
58178
})
59179
}
180+
181+
func printServices(out io.Writer, services []swarmtypes.Service) error {
182+
w := tabwriter.NewWriter(out, 0, 0, 1, ' ', 0)
183+
printHeaders(w)
184+
185+
for _, service := range services {
186+
printValues(w, &service)
187+
}
188+
return w.Flush()
189+
}
190+
191+
func printHeaders(w io.Writer) {
192+
var headers []string
193+
for _, column := range listColumns {
194+
headers = append(headers, column.header)
195+
}
196+
fmt.Fprintln(w, strings.Join(headers, "\t"))
197+
}
198+
199+
func printValues(w io.Writer, service *swarmtypes.Service) {
200+
var values []string
201+
for _, column := range listColumns {
202+
values = append(values, column.value(service))
203+
}
204+
fmt.Fprintln(w, strings.Join(values, "\t"))
205+
}

internal/commands/inspect.go

Lines changed: 51 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,25 @@
11
package commands
22

33
import (
4+
"bytes"
5+
"encoding/json"
46
"fmt"
57
"os"
6-
"strings"
78

89
"github.com/deislabs/cnab-go/action"
10+
"github.com/deislabs/cnab-go/bundle"
911
"github.com/docker/app/internal"
12+
"github.com/docker/app/internal/cliopts"
1013
"github.com/docker/app/internal/cnab"
1114
"github.com/docker/app/internal/inspect"
1215
"github.com/docker/cli/cli"
1316
"github.com/docker/cli/cli/command"
14-
"github.com/docker/cli/cli/command/stack"
15-
"github.com/docker/cli/cli/command/stack/options"
16-
"github.com/docker/cli/opts"
1717
"github.com/spf13/cobra"
18-
"github.com/spf13/pflag"
1918
)
2019

2120
type inspectOptions struct {
2221
credentialOptions
22+
cliopts.InstallerContextOptions
2323
pretty bool
2424
orchestrator string
2525
kubeNamespace string
@@ -29,10 +29,10 @@ func inspectCmd(dockerCli command.Cli) *cobra.Command {
2929
var opts inspectOptions
3030
cmd := &cobra.Command{
3131
Use: "inspect [OPTIONS] RUNNING_APP",
32-
Short: "Shows installation and application metadata, parameters and the containers list of a running application",
32+
Short: "Shows installation and App metadata, parameters and the service list of a running App",
3333
Example: `$ docker app inspect my-running-app
3434
$ docker app inspect my-running-app:1.0.0`,
35-
Args: cli.RequiresMaxArgs(1),
35+
Args: cli.ExactArgs(1),
3636
Hidden: true,
3737
RunE: func(cmd *cobra.Command, args []string) error {
3838
return runInspect(dockerCli, firstOrEmpty(args), opts)
@@ -42,6 +42,7 @@ $ docker app inspect my-running-app:1.0.0`,
4242
cmd.Flags().StringVar(&opts.orchestrator, "orchestrator", "", "Orchestrator where the App is running on (swarm, kubernetes)")
4343
cmd.Flags().StringVar(&opts.kubeNamespace, "namespace", "default", "Kubernetes namespace in which to find the App")
4444
opts.credentialOptions.addFlags(cmd.Flags())
45+
opts.InstallerContextOptions.AddFlags(cmd.Flags())
4546
return cmd
4647
}
4748

@@ -50,18 +51,9 @@ func runInspect(dockerCli command.Cli, appName string, inspectOptions inspectOpt
5051
if err != nil {
5152
return err
5253
}
53-
services, err := stack.GetServices(dockerCli, pflag.NewFlagSet("", pflag.ContinueOnError), orchestrator, options.Services{
54-
Filter: opts.NewFilterOpt(),
55-
Namespace: inspectOptions.kubeNamespace,
56-
})
57-
if err != nil {
58-
return err
59-
}
60-
println(services)
6154

62-
inspectOptions.SetDefaultTargetContext(dockerCli)
6355
defer muteDockerCli(dockerCli)()
64-
_, installationStore, credentialStore, err := prepareStores(inspectOptions.targetContext)
56+
_, installationStore, credentialStore, err := prepareStores(dockerCli.CurrentContext())
6557
if err != nil {
6658
return err
6759
}
@@ -75,48 +67,53 @@ func runInspect(dockerCli command.Cli, appName string, inspectOptions inspectOpt
7567
orchestratorName = string(orchestrator)
7668
}
7769

78-
format := "json"
79-
actionName := internal.ActionStatusJSONName
80-
if inspectOptions.pretty {
81-
format = "pretty"
82-
actionName = internal.ActionStatusName
83-
}
84-
85-
if err := inspect.Inspect(os.Stdout, installation.Claim, format, orchestratorName); err != nil {
70+
creds, err := prepareCredentialSet(installation.Bundle, inspectOptions.CredentialSetOpts(dockerCli, credentialStore)...)
71+
if err != nil {
8672
return err
8773
}
8874

89-
var statusAction bool
90-
for key := range installation.Bundle.Actions {
91-
if strings.HasPrefix(key, "io.cnab.status") {
92-
statusAction = true
93-
}
94-
}
95-
if !statusAction {
96-
return nil
97-
}
98-
99-
bind, err := cnab.RequiredBindMount(inspectOptions.targetContext, orchestratorName, dockerCli.ContextStore())
75+
var buf bytes.Buffer
76+
driverImpl, errBuf, err := cnab.SetupDriver(installation, dockerCli, inspectOptions.InstallerContextOptions, &buf)
10077
if err != nil {
10178
return err
10279
}
10380

104-
driverImpl, errBuf := cnab.PrepareDriver(dockerCli, bind, nil)
10581
a := &action.RunCustom{
106-
Action: actionName,
10782
Driver: driverImpl,
10883
}
109-
110-
creds, err := prepareCredentialSet(installation.Bundle, inspectOptions.CredentialSetOpts(dockerCli, credentialStore)...)
111-
if err != nil {
112-
return err
84+
if inspectOptions.pretty && hasAction(installation.Bundle, internal.ActionStatusName) {
85+
a.Action = internal.ActionStatusName
86+
} else if hasAction(installation.Bundle, internal.ActionStatusJSONName) {
87+
a.Action = internal.ActionStatusJSONName
88+
} else {
89+
return fmt.Errorf("inspect failed: status action is not supported by the App")
11390
}
114-
115-
installation.SetParameter(internal.ParameterInspectFormatName, format)
116-
println()
11791
if err := a.Run(&installation.Claim, creds, nil); err != nil {
11892
return fmt.Errorf("inspect failed: %s\n%s", err, errBuf)
11993
}
94+
95+
if inspectOptions.pretty {
96+
if err := inspect.Inspect(os.Stdout, installation, "pretty", orchestratorName); err != nil {
97+
return err
98+
}
99+
fmt.Fprint(os.Stdout, buf.String())
100+
} else {
101+
var statusJSON interface{}
102+
if err := json.Unmarshal(buf.Bytes(), &statusJSON); err != nil {
103+
return err
104+
}
105+
js, err := json.MarshalIndent(struct {
106+
AppInfo inspect.AppInfo `json:",omitempty"`
107+
Services interface{} `json:",omitempty"`
108+
}{
109+
inspect.GetAppInfo(installation, orchestratorName),
110+
statusJSON,
111+
}, "", " ")
112+
if err != nil {
113+
return err
114+
}
115+
fmt.Fprint(os.Stdout, string(js))
116+
}
120117
return nil
121118
}
122119

@@ -132,3 +129,12 @@ func getContextOrchestrator(dockerCli command.Cli, orchestratorFlag string) (com
132129
}
133130
return orchestrator, nil
134131
}
132+
133+
func hasAction(bndl *bundle.Bundle, actionName string) bool {
134+
for key := range bndl.Actions {
135+
if key == actionName {
136+
return true
137+
}
138+
}
139+
return false
140+
}

0 commit comments

Comments
 (0)