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

Commit bb9310f

Browse files
Add compose file summary (services, networks, volumes and secrets) to the inspect command
* Refactor inspect command Signed-off-by: Silvin Lubecki <silvin.lubecki@docker.com>
1 parent 66ee35d commit bb9310f

11 files changed

Lines changed: 258 additions & 54 deletions

File tree

cmd/docker-app/inspect.go

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,24 +3,37 @@ package main
33
import (
44
"github.com/docker/app/internal/inspect"
55
"github.com/docker/app/internal/packager"
6+
"github.com/docker/app/types"
67
"github.com/docker/cli/cli"
78
"github.com/docker/cli/cli/command"
9+
cliopts "github.com/docker/cli/opts"
810
"github.com/spf13/cobra"
911
)
1012

13+
var (
14+
inspectSettingsFile []string
15+
inspectEnv []string
16+
)
17+
1118
// inspectCmd represents the inspect command
1219
func inspectCmd(dockerCli command.Cli) *cobra.Command {
13-
return &cobra.Command{
14-
Use: "inspect [<app-name>]",
15-
Short: "Shows metadata and settings for a given application",
20+
cmd := &cobra.Command{
21+
Use: "inspect [<app-name>] [-s key=value...] [-f settings-file...]",
22+
Short: "Shows metadata, settings and summary of compose file for a given application",
1623
Args: cli.RequiresMaxArgs(1),
1724
RunE: func(cmd *cobra.Command, args []string) error {
18-
app, err := packager.Extract(firstOrEmpty(args))
25+
app, err := packager.Extract(firstOrEmpty(args),
26+
types.WithSettingsFiles(inspectSettingsFile...),
27+
)
1928
if err != nil {
2029
return err
2130
}
2231
defer app.Cleanup()
23-
return inspect.Inspect(dockerCli.Out(), app)
32+
argSettings := cliopts.ConvertKVStringsToMap(inspectEnv)
33+
return inspect.Inspect(dockerCli.Out(), app, argSettings)
2434
},
2535
}
36+
cmd.Flags().StringArrayVarP(&inspectSettingsFile, "settings-files", "f", []string{}, "Override settings files")
37+
cmd.Flags().StringArrayVarP(&inspectEnv, "set", "s", []string{}, "Override settings values")
38+
return cmd
2639
}
Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,14 @@
11
myapp 0.1.0
2+
23
Maintained by: bearclaw <bearclaw@bearclaw.bearclaw>
34

4-
Setting Default
5-
------- -------
5+
Service Replicas Ports Image
6+
------- -------- ----- -----
7+
test 1 alpine:latest
8+
9+
Setting Value
10+
------- -----
611
myapp.alpine_version latest
712
myapp.command1 cat
813
myapp.command2 foo
14+
myapp.command3 bar
Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,14 @@
11
hello-world 0.1.0
2+
23
Maintained by: user <user@email.com>
34

45
Hello, World!
56

6-
Setting Default
7-
------- -------
7+
Service Replicas Ports Image
8+
------- -------- ----- -----
9+
hello 1 8080 hashicorp/http-echo
10+
11+
Setting Value
12+
------- -----
813
port 8080
914
text Hello, World!

e2e/testdata/render/envvariables/my.dockerapp/settings.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@ myapp:
22
alpine_version: latest
33
command1: cat
44
command2: foo
5+
command3: bar

internal/inspect/inspect.go

Lines changed: 142 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,163 @@
11
package inspect
22

33
import (
4+
"bytes"
45
"fmt"
56
"io"
67
"sort"
8+
"strings"
79
"text/tabwriter"
810

11+
"github.com/docker/app/render"
912
"github.com/docker/app/types"
13+
"github.com/docker/app/types/settings"
14+
composetypes "github.com/docker/cli/cli/compose/types"
1015
)
1116

1217
// Inspect dumps the metadata of an app
13-
func Inspect(out io.Writer, app *types.App) error {
18+
func Inspect(out io.Writer, app *types.App, argSettings map[string]string) error {
19+
// Render the compose file
20+
config, err := render.Render(app, argSettings)
21+
if err != nil {
22+
return err
23+
}
24+
25+
// Extract all the settings
26+
settingsKeys, allSettings, err := extractSettings(app, argSettings)
27+
if err != nil {
28+
return err
29+
}
30+
31+
var sections []*bytes.Buffer
32+
33+
// Add Meta data
34+
addMetadata(&sections, app)
35+
36+
// Add Service section
37+
addSection(&sections, len(config.Services), func(w io.Writer) {
38+
for _, service := range config.Services {
39+
fmt.Fprintf(w, "%s\t%d\t%s\t%s\n", service.Name, getReplicas(service), getPorts(service), service.Image)
40+
}
41+
}, "Service", "Replicas", "Ports", "Image")
42+
43+
// Add Network section
44+
addSection(&sections, len(config.Networks), func(w io.Writer) {
45+
for name := range config.Networks {
46+
fmt.Fprintln(w, name)
47+
}
48+
}, "Network")
49+
50+
// Add Volume section
51+
addSection(&sections, len(config.Volumes), func(w io.Writer) {
52+
for name := range config.Volumes {
53+
fmt.Fprintln(w, name)
54+
}
55+
}, "Volume")
56+
57+
// Add Secret section
58+
addSection(&sections, len(config.Secrets), func(w io.Writer) {
59+
for name := range config.Secrets {
60+
fmt.Fprintln(w, name)
61+
}
62+
}, "Secret")
63+
64+
// Add Setting section
65+
addSection(&sections, len(settingsKeys), func(w io.Writer) {
66+
for _, k := range settingsKeys {
67+
fmt.Fprintf(w, "%s\t%s\n", k, allSettings[k])
68+
}
69+
}, "Setting", "Value")
70+
71+
// Print all sections
72+
printSections(out, sections)
73+
74+
return nil
75+
}
76+
77+
func addMetadata(sections *[]*bytes.Buffer, app *types.App) {
78+
buf := &bytes.Buffer{}
1479
meta := app.Metadata()
15-
// extract settings
16-
settings := app.Settings().Flatten()
80+
fmt.Fprintln(buf, meta.Name, meta.Version)
81+
if maintainers := meta.Maintainers.String(); maintainers != "" {
82+
fmt.Fprintln(buf)
83+
fmt.Fprintln(buf, "Maintained by:", maintainers)
84+
}
85+
if meta.Description != "" {
86+
fmt.Fprintln(buf)
87+
fmt.Fprintln(buf, meta.Description)
88+
}
89+
*sections = append(*sections, buf)
90+
}
91+
92+
func addSection(sections *[]*bytes.Buffer, len int, printer func(io.Writer), headers ...string) {
93+
if len == 0 {
94+
return
95+
}
96+
buf := &bytes.Buffer{}
97+
w := tabwriter.NewWriter(buf, 0, 0, 1, ' ', 0)
98+
printHeaders(w, headers...)
99+
printer(w)
100+
w.Flush()
101+
*sections = append(*sections, buf)
102+
}
103+
104+
// printSections makes sure there isn't any extra line at the end of the command output
105+
func printSections(out io.Writer, sections []*bytes.Buffer) {
106+
ss := make([]string, len(sections))
107+
for i, section := range sections {
108+
ss[i] = section.String()
109+
}
110+
fmt.Fprint(out, strings.Join(ss, "\n"))
111+
}
112+
113+
func printHeaders(w io.Writer, headers ...string) {
114+
fmt.Fprintln(w, strings.Join(headers, "\t"))
115+
dashes := make([]string, len(headers))
116+
for i, h := range headers {
117+
dashes[i] = strings.Repeat("-", len(h))
118+
}
119+
fmt.Fprintln(w, strings.Join(dashes, "\t"))
120+
}
121+
122+
func getReplicas(service composetypes.ServiceConfig) int {
123+
if service.Deploy.Replicas != nil {
124+
return int(*service.Deploy.Replicas)
125+
}
126+
return 1
127+
}
128+
129+
func getPorts(service composetypes.ServiceConfig) string {
130+
var ports []string
131+
for _, port := range service.Ports {
132+
if port.Published > 0 {
133+
ports = append(ports, fmt.Sprintf("%d", port.Published))
134+
}
135+
}
136+
return strings.Join(ports, ",")
137+
}
138+
139+
func extractSettings(app *types.App, argSettings map[string]string) ([]string, map[string]string, error) {
140+
allSettings, err := mergeAndFlattenSettings(app, argSettings)
141+
if err != nil {
142+
return nil, nil, err
143+
}
17144
// sort the keys to get consistent output
18145
var settingsKeys []string
19-
for k := range settings {
146+
for k := range allSettings {
20147
settingsKeys = append(settingsKeys, k)
21148
}
22149
sort.Slice(settingsKeys, func(i, j int) bool { return settingsKeys[i] < settingsKeys[j] })
23-
// build maintainers string
24-
maintainers := meta.Maintainers.String()
25-
fmt.Fprintf(out, "%s %s\n", meta.Name, meta.Version)
26-
if maintainers != "" {
27-
fmt.Fprintf(out, "Maintained by: %s\n", maintainers)
28-
fmt.Fprintln(out, "")
29-
}
30-
if meta.Description != "" {
31-
fmt.Fprintf(out, "%s\n", meta.Description)
32-
fmt.Fprintln(out, "")
150+
return settingsKeys, allSettings, nil
151+
}
152+
153+
func mergeAndFlattenSettings(app *types.App, argSettings map[string]string) (map[string]string, error) {
154+
sArgs, err := settings.FromFlatten(argSettings)
155+
if err != nil {
156+
return nil, err
33157
}
34-
w := tabwriter.NewWriter(out, 0, 0, 1, ' ', 0)
35-
fmt.Fprintln(w, "Setting\tDefault")
36-
fmt.Fprintln(w, "-------\t-------")
37-
for _, k := range settingsKeys {
38-
fmt.Fprintf(w, "%s\t%s\n", k, settings[k])
158+
s, err := settings.Merge(app.Settings(), sArgs)
159+
if err != nil {
160+
return nil, err
39161
}
40-
w.Flush()
41-
return nil
162+
return s.Flatten(), nil
42163
}

internal/inspect/inspect_test.go

Lines changed: 52 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,7 @@ import (
1313
)
1414

1515
const (
16-
composeYAML = `version: "3.1"
17-
18-
services:
19-
web:
20-
image: nginx`
16+
composeYAML = `version: "3.1"`
2117
)
2218

2319
func TestInspect(t *testing.T) {
@@ -50,8 +46,41 @@ maintainers:
5046
description: "this is sparta !"`),
5147
fs.WithFile(internal.SettingsFileName, ""),
5248
),
49+
fs.WithDir("overridden",
50+
fs.WithFile(internal.ComposeFileName, `
51+
version: "3.1"
52+
53+
services:
54+
web:
55+
image: nginx
56+
ports:
57+
- ${web.port}:80
58+
`),
59+
fs.WithFile(internal.MetadataFileName, `
60+
version: 0.1.0
61+
name: foo
62+
`),
63+
fs.WithFile(internal.SettingsFileName, ""),
64+
),
5365
fs.WithDir("full",
54-
fs.WithFile(internal.ComposeFileName, composeYAML),
66+
fs.WithFile(internal.ComposeFileName, `
67+
version: "3.1"
68+
69+
services:
70+
web:
71+
image: nginx:latest
72+
ports:
73+
- 8080:80
74+
deploy:
75+
replicas: 2
76+
networks:
77+
my-network:
78+
volumes:
79+
my-volume:
80+
secrets:
81+
my-secret:
82+
file: ./my_secret.txt
83+
`),
5584
fs.WithFile(internal.MetadataFileName, `
5685
version: 0.1.0
5786
name: foo
@@ -66,14 +95,23 @@ text: hello`),
6695
)
6796
defer dir.Remove()
6897

69-
for _, appname := range []string{
70-
"no-maintainers", "no-description", "no-settings", "full",
98+
for _, testcase := range []struct {
99+
name string
100+
args map[string]string
101+
}{
102+
{name: "no-maintainers"},
103+
{name: "no-description"},
104+
{name: "no-settings"},
105+
{name: "overridden", args: map[string]string{"web.port": "80"}},
106+
{name: "full"},
71107
} {
72-
outBuffer := new(bytes.Buffer)
73-
app, err := types.NewAppFromDefaultFiles(dir.Join(appname))
74-
assert.NilError(t, err)
75-
err = Inspect(outBuffer, app)
76-
assert.NilError(t, err)
77-
golden.Assert(t, outBuffer.String(), fmt.Sprintf("inspect-%s.golden", appname), appname)
108+
t.Run(testcase.name, func(t *testing.T) {
109+
outBuffer := new(bytes.Buffer)
110+
app, err := types.NewAppFromDefaultFiles(dir.Join(testcase.name))
111+
assert.NilError(t, err)
112+
err = Inspect(outBuffer, app, testcase.args)
113+
assert.NilError(t, err)
114+
golden.Assert(t, outBuffer.String(), fmt.Sprintf("inspect-%s.golden", testcase.name), testcase.name)
115+
})
78116
}
79117
}
Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,26 @@
11
foo 0.1.0
2+
23
Maintained by: foo <foo@bar.com>
34

45
this is sparta !
56

6-
Setting Default
7-
------- -------
7+
Service Replicas Ports Image
8+
------- -------- ----- -----
9+
web 2 8080 nginx:latest
10+
11+
Network
12+
-------
13+
my-network
14+
15+
Volume
16+
------
17+
my-volume
18+
19+
Secret
20+
------
21+
my-secret
22+
23+
Setting Value
24+
------- -----
825
port 8080
926
text hello
Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
11
foo 0.1.0
2-
Maintained by: foo <foo@bar.com>
32

4-
Setting Default
5-
------- -------
3+
Maintained by: foo <foo@bar.com>
Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1 @@
11
foo 0.1.0
2-
Setting Default
3-
------- -------

0 commit comments

Comments
 (0)