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

Commit 9747a3f

Browse files
authored
Merge pull request #357 from shin-/json-formatter
Add a JSON format option to docker-app render
2 parents aef230a + 6c24107 commit 9747a3f

12 files changed

Lines changed: 348 additions & 4 deletions

File tree

cmd/docker-app/render.go

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ import (
55
"os"
66

77
"github.com/docker/app/internal"
8+
"github.com/docker/app/internal/formatter"
89
"github.com/docker/app/internal/packager"
9-
"github.com/docker/app/internal/yaml"
1010
"github.com/docker/app/render"
1111
"github.com/docker/app/types"
1212
"github.com/docker/cli/cli"
@@ -16,6 +16,7 @@ import (
1616
)
1717

1818
var (
19+
formatDriver string
1920
renderComposeFiles []string
2021
renderSettingsFile []string
2122
renderEnv []string
@@ -42,18 +43,18 @@ func renderCmd(dockerCli command.Cli) *cobra.Command {
4243
if err != nil {
4344
return err
4445
}
45-
res, err := yaml.Marshal(rendered)
46+
res, err := formatter.Format(rendered, formatDriver)
4647
if err != nil {
4748
return err
4849
}
4950
if renderOutput == "-" {
50-
fmt.Fprint(dockerCli.Out(), string(res))
51+
fmt.Fprint(dockerCli.Out(), res)
5152
} else {
5253
f, err := os.Create(renderOutput)
5354
if err != nil {
5455
return err
5556
}
56-
fmt.Fprint(f, string(res))
57+
fmt.Fprint(f, res)
5758
}
5859
return nil
5960
},
@@ -68,5 +69,6 @@ func renderCmd(dockerCli command.Cli) *cobra.Command {
6869
cmd.Flags().StringArrayVarP(&renderSettingsFile, "settings-files", "f", []string{}, "Override settings files")
6970
cmd.Flags().StringArrayVarP(&renderEnv, "set", "s", []string{}, "Override settings values")
7071
cmd.Flags().StringVarP(&renderOutput, "output", "o", "-", "Output file")
72+
cmd.Flags().StringVarP(&formatDriver, "presenter", "p", "yaml", "Configure the output format (yaml|json)")
7173
return cmd
7274
}

e2e/commands_test.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,15 @@ func testRenderApp(appPath string, env ...string) func(*testing.T) {
7676
}
7777
}
7878

79+
func TestRenderFormatters(t *testing.T) {
80+
appPath := filepath.Join("testdata", "fork", "simple.dockerapp")
81+
result := icmd.RunCommand(dockerApp, "render", "-p", "json", appPath).Assert(t, icmd.Success)
82+
assert.Assert(t, golden.String(result.Stdout(), "expected-json-render.golden"))
83+
84+
result = icmd.RunCommand(dockerApp, "render", "-p", "yaml", appPath).Assert(t, icmd.Success)
85+
assert.Assert(t, golden.String(result.Stdout(), "expected-yaml-render.golden"))
86+
}
87+
7988
func TestInit(t *testing.T) {
8089
composeData := `version: "3.2"
8190
services:
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
{
2+
"networks": {
3+
"back": {
4+
"ipam": {},
5+
"external": false
6+
},
7+
"front": {
8+
"ipam": {},
9+
"external": false
10+
}
11+
},
12+
"services": {
13+
"api": {
14+
"build": {},
15+
"credential_spec": {},
16+
"deploy": {
17+
"resources": {},
18+
"placement": {}
19+
},
20+
"image": "python:3.6",
21+
"networks": {
22+
"back": null,
23+
"front": {
24+
"aliases": [
25+
"corp.app.api.com",
26+
"coolapp.com"
27+
]
28+
}
29+
}
30+
},
31+
"db": {
32+
"build": {},
33+
"credential_spec": {},
34+
"deploy": {
35+
"resources": {},
36+
"placement": {}
37+
},
38+
"image": "postgres:9.3",
39+
"networks": {
40+
"back": null
41+
}
42+
},
43+
"web": {
44+
"build": {},
45+
"credential_spec": {},
46+
"deploy": {
47+
"resources": {},
48+
"placement": {}
49+
},
50+
"image": "nginx:latest",
51+
"networks": {
52+
"front": null
53+
},
54+
"ports": [
55+
{
56+
"mode": "ingress",
57+
"target": 80,
58+
"published": 8082,
59+
"protocol": "tcp"
60+
}
61+
],
62+
"volumes": [
63+
{
64+
"type": "volume",
65+
"source": "static",
66+
"target": "/opt/data/static"
67+
}
68+
]
69+
}
70+
},
71+
"version": "3.6",
72+
"volumes": {
73+
"static": {
74+
"name": "corp/web-static-data",
75+
"external": true
76+
}
77+
}
78+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
version: "3.6"
2+
services:
3+
api:
4+
image: python:3.6
5+
networks:
6+
back: null
7+
front:
8+
aliases:
9+
- corp.app.api.com
10+
- coolapp.com
11+
db:
12+
image: postgres:9.3
13+
networks:
14+
back: null
15+
web:
16+
image: nginx:latest
17+
networks:
18+
front: null
19+
ports:
20+
- mode: ingress
21+
target: 80
22+
published: 8082
23+
protocol: tcp
24+
volumes:
25+
- type: volume
26+
source: static
27+
target: /opt/data/static
28+
networks:
29+
back: {}
30+
front: {}
31+
volumes:
32+
static:
33+
name: corp/web-static-data
34+
external: true
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package driver
2+
3+
import (
4+
composetypes "github.com/docker/cli/cli/compose/types"
5+
)
6+
7+
// Driver is the interface that must be implemented by a formatter driver.
8+
type Driver interface {
9+
// Format executes the formatter on the source config
10+
Format(config *composetypes.Config) (string, error)
11+
}

internal/formatter/formatter.go

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
package formatter
2+
3+
import (
4+
"sort"
5+
"sync"
6+
7+
"github.com/docker/app/internal/formatter/driver"
8+
composetypes "github.com/docker/cli/cli/compose/types"
9+
"github.com/pkg/errors"
10+
)
11+
12+
var (
13+
driversMu sync.RWMutex
14+
drivers = map[string]driver.Driver{}
15+
)
16+
17+
// Register makes a formatter available by the provided name.
18+
// If Register is called twice with the same name or if driver is nil,
19+
// it panics.
20+
func Register(name string, driver driver.Driver) {
21+
driversMu.Lock()
22+
defer driversMu.Unlock()
23+
if driver == nil {
24+
panic("formatter: Register driver is nil")
25+
}
26+
if _, dup := drivers[name]; dup {
27+
panic("formatter: Register called twice for driver " + name)
28+
}
29+
drivers[name] = driver
30+
}
31+
32+
// Format uses the specified formatter to create a printable output.
33+
// If the formatter is not registered, this errors out.
34+
func Format(config *composetypes.Config, formatter string) (string, error) {
35+
driversMu.RLock()
36+
d, present := drivers[formatter]
37+
driversMu.RUnlock()
38+
if !present {
39+
return "", errors.Errorf("unknown presenter %q", formatter)
40+
}
41+
s, err := d.Format(config)
42+
if err != nil {
43+
return "", err
44+
}
45+
return s, nil
46+
}
47+
48+
// Drivers returns a sorted list of the names of the registered drivers.
49+
func Drivers() []string {
50+
list := []string{}
51+
driversMu.RLock()
52+
for name := range drivers {
53+
list = append(list, name)
54+
}
55+
driversMu.RUnlock()
56+
sort.Strings(list)
57+
return list
58+
}
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
package formatter
2+
3+
import (
4+
"testing"
5+
6+
"github.com/docker/app/internal/formatter/driver"
7+
composetypes "github.com/docker/cli/cli/compose/types"
8+
"github.com/pkg/errors"
9+
"gotest.tools/assert"
10+
is "gotest.tools/assert/cmp"
11+
)
12+
13+
type fakeDriver struct{}
14+
15+
func (d *fakeDriver) Format(config *composetypes.Config) (string, error) {
16+
return "fake", nil
17+
}
18+
19+
type fakeErrorDriver struct{}
20+
21+
func (d *fakeErrorDriver) Format(config *composetypes.Config) (string, error) {
22+
return "", errors.New("error in driver")
23+
}
24+
25+
func TestRegisterNilPanics(t *testing.T) {
26+
defer func() {
27+
if recover() == nil {
28+
t.Errorf("The code did not panic")
29+
}
30+
resetDrivers()
31+
}()
32+
Register("foo", nil)
33+
}
34+
35+
func TestRegisterDuplicatePanics(t *testing.T) {
36+
defer func() {
37+
if recover() == nil {
38+
t.Errorf("The code did not panic")
39+
}
40+
resetDrivers()
41+
}()
42+
Register("bar", &fakeDriver{})
43+
Register("bar", &fakeDriver{})
44+
}
45+
46+
func TestRegister(t *testing.T) {
47+
d := &fakeDriver{}
48+
Register("baz", d)
49+
defer resetDrivers()
50+
assert.Check(t, is.DeepEqual(drivers, map[string]driver.Driver{"baz": d}))
51+
}
52+
53+
func TestNoDrivers(t *testing.T) {
54+
assert.Check(t, is.DeepEqual(Drivers(), []string{}))
55+
}
56+
57+
func TestRegisteredDrivers(t *testing.T) {
58+
Register("foo", &fakeDriver{})
59+
Register("bar", &fakeDriver{})
60+
defer resetDrivers()
61+
assert.Check(t, is.DeepEqual(Drivers(), []string{"bar", "foo"}))
62+
}
63+
64+
func TestFormatNonExistentDriver(t *testing.T) {
65+
_, err := Format(&composetypes.Config{}, "toto")
66+
assert.Check(t, err != nil)
67+
assert.Check(t, is.ErrorContains(err, `unknown presenter "toto"`))
68+
}
69+
70+
func TestFormatErrorDriver(t *testing.T) {
71+
Register("err", &fakeErrorDriver{})
72+
defer resetDrivers()
73+
_, err := Format(&composetypes.Config{}, "err")
74+
assert.Check(t, err != nil)
75+
assert.Check(t, is.ErrorContains(err, "error in driver"))
76+
}
77+
78+
func TestFormatNone(t *testing.T) {
79+
Register("fake", &fakeDriver{})
80+
defer resetDrivers()
81+
_, err := Format(&composetypes.Config{}, "none")
82+
assert.Check(t, err != nil)
83+
assert.Check(t, is.ErrorContains(err, `unknown presenter "none"`))
84+
}
85+
86+
func TestFormat(t *testing.T) {
87+
Register("fake", &fakeDriver{})
88+
defer resetDrivers()
89+
s, err := Format(&composetypes.Config{}, "fake")
90+
assert.NilError(t, err)
91+
assert.Check(t, is.Equal(s, "fake"))
92+
}
93+
94+
func resetDrivers() {
95+
drivers = map[string]driver.Driver{}
96+
}

internal/formatter/json/doc.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
package json

internal/formatter/json/driver.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package json
2+
3+
import (
4+
"encoding/json"
5+
6+
"github.com/docker/app/internal/formatter"
7+
composetypes "github.com/docker/cli/cli/compose/types"
8+
"github.com/pkg/errors"
9+
)
10+
11+
func init() {
12+
formatter.Register("json", &Driver{})
13+
}
14+
15+
// Driver is the json implementation of formatter drivers.
16+
type Driver struct{}
17+
18+
// Format creates a JSON document from the source config.
19+
func (d *Driver) Format(config *composetypes.Config) (string, error) {
20+
result, err := json.MarshalIndent(config, "", " ")
21+
if err != nil {
22+
return "", errors.Wrap(err, "failed to produce json structure")
23+
}
24+
return string(result) + "\n", nil
25+
}

internal/formatter/yaml/doc.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
package yaml

0 commit comments

Comments
 (0)