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

Commit 4d8a762

Browse files
authored
Merge pull request #115 from mnottale/single-file
Support single-file mode: experimental merge/split commands, init -s.
2 parents 980b066 + 6b2f51c commit 4d8a762

File tree

8 files changed

+256
-8
lines changed

8 files changed

+256
-8
lines changed

cmd/init.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,17 +13,19 @@ var initCmd = &cobra.Command{
1313
Long: `Start building a Docker application. Will automatically detect a docker-compose.yml file in the current directory.`,
1414
Args: cli.ExactArgs(1),
1515
RunE: func(cmd *cobra.Command, args []string) error {
16-
return packager.Init(args[0], initComposeFile, initDescription, initMaintainers)
16+
return packager.Init(args[0], initComposeFile, initDescription, initMaintainers, initSingleFile)
1717
},
1818
}
1919

2020
var initComposeFile string
2121
var initDescription string
2222
var initMaintainers []string
23+
var initSingleFile bool
2324

2425
func init() {
2526
rootCmd.AddCommand(initCmd)
2627
initCmd.Flags().StringVarP(&initComposeFile, "compose-file", "c", "", "Initial Compose file (optional)")
2728
initCmd.Flags().StringVarP(&initDescription, "description", "d", "", "Initial description (optional)")
2829
initCmd.Flags().StringArrayVarP(&initMaintainers, "maintainer", "m", []string{}, "Maintainer (name:email) (optional)")
30+
initCmd.Flags().BoolVarP(&initSingleFile, "single-file", "s", false, "Create a single-file application")
2931
}

cmd/merge.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package cmd
2+
3+
import (
4+
"github.com/docker/cli/cli"
5+
"github.com/docker/lunchbox/internal"
6+
"github.com/docker/lunchbox/packager"
7+
"github.com/spf13/cobra"
8+
)
9+
10+
var mergeCmd = &cobra.Command{
11+
Use: "merge [<app-name>] [-o output_dir]",
12+
Short: "Merge the application as a single file multi-document YAML",
13+
Args: cli.RequiresMaxArgs(1),
14+
RunE: func(cmd *cobra.Command, args []string) error {
15+
return packager.Merge(firstOrEmpty(args), mergeOutputFile)
16+
},
17+
}
18+
19+
var mergeOutputFile string
20+
21+
func init() {
22+
if internal.Experimental == "on" {
23+
rootCmd.AddCommand(mergeCmd)
24+
mergeCmd.Flags().StringVarP(&mergeOutputFile, "output", "o", "-", "Output file (default: stdout)")
25+
}
26+
}

cmd/split.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package cmd
2+
3+
import (
4+
"github.com/docker/cli/cli"
5+
"github.com/docker/lunchbox/internal"
6+
"github.com/docker/lunchbox/packager"
7+
"github.com/spf13/cobra"
8+
)
9+
10+
var splitCmd = &cobra.Command{
11+
Use: "split [<app-name>] [-o output_dir]",
12+
Short: "Split a single-file application into multiple files",
13+
Args: cli.RequiresMaxArgs(1),
14+
RunE: func(cmd *cobra.Command, args []string) error {
15+
return packager.Split(firstOrEmpty(args), splitOutputDir)
16+
},
17+
}
18+
19+
var splitOutputDir string
20+
21+
func init() {
22+
if internal.Experimental == "on" {
23+
rootCmd.AddCommand(splitCmd)
24+
splitCmd.Flags().StringVarP(&splitOutputDir, "output", "o", "-", "Output directory")
25+
}
26+
}

e2e/binary_test.go

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,28 @@ targets:
166166
)
167167

168168
assert.Assert(t, fs.Equal(dirName, manifest))
169+
170+
// test single-file init
171+
args = []string{
172+
"init",
173+
"tac",
174+
"-c",
175+
filepath.Join(inputDir, "docker-compose.yml"),
176+
"-d",
177+
"my cool app",
178+
"-m", "bob",
179+
"-m", "joe:[email protected]",
180+
"-s",
181+
}
182+
cmd = exec.Command(dockerApp, args...)
183+
output, err = cmd.CombinedOutput()
184+
if err != nil {
185+
fmt.Println(string(output))
186+
}
187+
assert.NilError(t, err)
188+
defer os.Remove("tac.dockerapp")
189+
appData, _ := ioutil.ReadFile("tac.dockerapp")
190+
golden.Assert(t, string(appData), "init-singlefile.dockerapp")
169191
}
170192

171193
func TestInspectBinary(t *testing.T) {
@@ -226,3 +248,41 @@ func TestHelmBinary(t *testing.T) {
226248
golden.AssertBytes(t, values, "helm-expected.chart/values.yaml")
227249
golden.AssertBytes(t, stack, "helm-expected.chart/templates/stack.yaml")
228250
}
251+
252+
func TestSplitMergeBinary(t *testing.T) {
253+
dockerApp, hasExperimental := getBinary(t)
254+
if !hasExperimental {
255+
t.Skip("experimental mode needed for this test")
256+
}
257+
app := "render/envvariables"
258+
cmd := exec.Command(dockerApp, "merge", app, "-o", "remerged.dockerapp")
259+
output, err := cmd.CombinedOutput()
260+
if err != nil {
261+
fmt.Println(string(output))
262+
}
263+
assert.NilError(t, err)
264+
defer os.Remove("remerged.dockerapp")
265+
// test that inspect works on single-file
266+
cmd = exec.Command(dockerApp, "inspect", "remerged")
267+
output, err = cmd.CombinedOutput()
268+
if err != nil {
269+
fmt.Println(string(output))
270+
}
271+
assert.NilError(t, err)
272+
golden.Assert(t, string(output), "envvariables-inspect.golden")
273+
// split it
274+
cmd = exec.Command(dockerApp, "split", "remerged", "-o", "splitted.dockerapp")
275+
output, err = cmd.CombinedOutput()
276+
if err != nil {
277+
fmt.Println(string(output))
278+
}
279+
assert.NilError(t, err)
280+
defer os.RemoveAll("splitted.dockerapp")
281+
cmd = exec.Command(dockerApp, "inspect", "splitted")
282+
output, err = cmd.CombinedOutput()
283+
if err != nil {
284+
fmt.Println(string(output))
285+
}
286+
assert.NilError(t, err)
287+
golden.Assert(t, string(output), "envvariables-inspect.golden")
288+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
version: 0.1.0
2+
name: tac
3+
description: my cool app
4+
maintainers:
5+
- name: bob
6+
email: ""
7+
- name: joe
8+
9+
targets:
10+
swarm: true
11+
kubernetes: true
12+
13+
--
14+
services:
15+
nginx:
16+
image: nginx:${NGINX_VERSION}
17+
command: nginx $NGINX_ARGS
18+
19+
--
20+
NGINX_ARGS: FILL ME
21+
NGINX_VERSION: latest

packager/extract.go

Lines changed: 41 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -76,23 +76,59 @@ func Extract(appname string) (string, func(), error) {
7676
// directory: already decompressed
7777
return appname, noop, nil
7878
}
79-
// not a dir: probably a tarball package, extract that in a temp dir
79+
// not a dir: single-file or a tarball package, extract that in a temp dir
8080
tempDir, err := ioutil.TempDir("", "dockerapp")
8181
if err != nil {
8282
return "", noop, err
8383
}
84+
defer func() {
85+
if err != nil {
86+
os.RemoveAll(tempDir)
87+
}
88+
}()
8489
appDir := filepath.Join(tempDir, filepath.Base(appname))
85-
if err := os.Mkdir(appDir, 0755); err != nil {
86-
os.RemoveAll(tempDir)
90+
if err = os.Mkdir(appDir, 0755); err != nil {
8791
return "", noop, err
8892
}
89-
if err = extract(appname, appDir); err != nil {
90-
os.RemoveAll(tempDir)
93+
if err = extract(appname, appDir); err == nil {
94+
return appDir, func() { os.RemoveAll(tempDir) }, nil
95+
}
96+
if err = extractSingleFile(appname, appDir); err != nil {
9197
return "", noop, err
9298
}
99+
// not a tarball, single-file then
93100
return appDir, func() { os.RemoveAll(tempDir) }, nil
94101
}
95102

103+
func extractSingleFile(appname, appDir string) error {
104+
// not a tarball, single-file then
105+
data, err := ioutil.ReadFile(appname)
106+
if err != nil {
107+
return err
108+
}
109+
parts := strings.Split(string(data), "\n--")
110+
if len(parts) != 3 {
111+
return fmt.Errorf("malformed single-file application: expected 3 documents")
112+
}
113+
names := []string{"metadata.yml", "docker-compose.yml", "settings.yml"}
114+
for i, p := range parts {
115+
data := ""
116+
if i == 0 {
117+
data = p
118+
} else {
119+
d := strings.SplitN(p, "\n", 2)
120+
if len(d) > 1 {
121+
data = d[1]
122+
}
123+
}
124+
err = ioutil.WriteFile(filepath.Join(appDir, names[i]), []byte(data), 0644)
125+
if err != nil {
126+
return err
127+
}
128+
}
129+
return nil
130+
}
131+
96132
func extract(appname, outputDir string) error {
97133
f, err := os.Open(appname)
98134
if err != nil {

packager/init.go

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import (
1717

1818
// Init is the entrypoint initialization function.
1919
// It generates a new application package based on the provided parameters.
20-
func Init(name string, composeFile string, description string, maintainers []string) error {
20+
func Init(name string, composeFile string, description string, maintainers []string, singleFile bool) error {
2121
if err := utils.ValidateAppName(name); err != nil {
2222
return err
2323
}
@@ -45,7 +45,20 @@ func Init(name string, composeFile string, description string, maintainers []str
4545
} else {
4646
err = initFromComposeFile(name, composeFile)
4747
}
48-
return err
48+
if err != nil {
49+
return err
50+
}
51+
if !singleFile {
52+
return nil
53+
}
54+
// Merge as a single file
55+
temp := "_temp_dockerapp__.dockerapp"
56+
err = os.Rename(dirName, temp)
57+
if err != nil {
58+
return err
59+
}
60+
defer os.RemoveAll(temp)
61+
return Merge(temp, dirName)
4962
}
5063

5164
func initFromScratch(name string) error {

packager/split.go

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
package packager
2+
3+
import (
4+
"io"
5+
"io/ioutil"
6+
"os"
7+
"path/filepath"
8+
)
9+
10+
// Split converts an app package to the split version
11+
func Split(appname string, outputDir string) error {
12+
appname, cleanup, err := Extract(appname)
13+
if err != nil {
14+
return err
15+
}
16+
defer cleanup()
17+
err = os.Mkdir(outputDir, 0755)
18+
if err != nil {
19+
return err
20+
}
21+
names := []string{"metadata.yml", "docker-compose.yml", "settings.yml"}
22+
for _, n := range names {
23+
input, err := ioutil.ReadFile(filepath.Join(appname, n))
24+
if err != nil {
25+
return err
26+
}
27+
err = ioutil.WriteFile(filepath.Join(outputDir, n), input, 0644)
28+
if err != nil {
29+
return err
30+
}
31+
}
32+
return nil
33+
}
34+
35+
// Merge converts an app-package to the single-file merged version
36+
func Merge(appname string, outputFile string) error {
37+
appname, cleanup, err := Extract(appname)
38+
if err != nil {
39+
return err
40+
}
41+
defer cleanup()
42+
var target io.Writer
43+
if outputFile == "-" {
44+
target = os.Stdout
45+
} else {
46+
target, err = os.Create(outputFile)
47+
if err != nil {
48+
return err
49+
}
50+
defer target.(io.WriteCloser).Close()
51+
}
52+
names := []string{"metadata.yml", "docker-compose.yml", "settings.yml"}
53+
for i, n := range names {
54+
input, err := ioutil.ReadFile(filepath.Join(appname, n))
55+
if err != nil {
56+
return err
57+
}
58+
target.Write(input)
59+
if i != 2 {
60+
io.WriteString(target, "\n--\n")
61+
}
62+
}
63+
return nil
64+
}

0 commit comments

Comments
 (0)