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

Commit fba6a09

Browse files
authored
Merge pull request #240 from mnottale/split-merge-inplace
split, merge: Handle in-place conversion, move out of experimental.
2 parents fc34e5f + 38091fe commit fba6a09

5 files changed

Lines changed: 121 additions & 42 deletions

File tree

cmd/docker-app/merge.go

Lines changed: 57 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,66 @@
11
package main
22

33
import (
4+
"fmt"
45
"io"
6+
"io/ioutil"
57
"os"
8+
"strings"
69

710
"github.com/docker/app/internal"
811
"github.com/docker/app/internal/packager"
912
"github.com/docker/cli/cli"
1013
"github.com/docker/cli/cli/command"
14+
"github.com/pkg/errors"
1115
"github.com/spf13/cobra"
1216
)
1317

1418
var mergeOutputFile string
1519

20+
// Check appname directory for extra files and return them
21+
func extraFiles(appname string) ([]string, error) {
22+
files, err := ioutil.ReadDir(appname)
23+
if err != nil {
24+
return nil, err
25+
}
26+
var res []string
27+
for _, f := range files {
28+
hit := false
29+
for _, afn := range internal.FileNames {
30+
if afn == f.Name() {
31+
hit = true
32+
break
33+
}
34+
}
35+
if !hit {
36+
res = append(res, f.Name())
37+
}
38+
}
39+
return res, nil
40+
}
41+
1642
func mergeCmd(dockerCli command.Cli) *cobra.Command {
1743
cmd := &cobra.Command{
18-
Use: "merge [<app-name>] [-o output_dir]",
44+
Use: "merge [<app-name>] [-o output_file]",
1945
Short: "Merge the application as a single file multi-document YAML",
2046
Args: cli.RequiresMaxArgs(1),
2147
RunE: func(cmd *cobra.Command, args []string) error {
22-
appname, cleanup, err := packager.Extract(firstOrEmpty(args))
48+
extractedApp, err := packager.ExtractWithOrigin(firstOrEmpty(args))
2349
if err != nil {
2450
return err
2551
}
26-
defer cleanup()
52+
defer extractedApp.Cleanup()
53+
inPlace := mergeOutputFile == ""
54+
if inPlace {
55+
extra, err := extraFiles(extractedApp.AppName)
56+
if err != nil {
57+
return errors.Wrap(err, "error scanning application directory")
58+
}
59+
if len(extra) != 0 {
60+
return fmt.Errorf("refusing to overwrite %s: extra files would be deleted: %s", extractedApp.OriginalAppName, strings.Join(extra, ","))
61+
}
62+
mergeOutputFile = extractedApp.OriginalAppName + ".tmp"
63+
}
2764
var target io.Writer
2865
if mergeOutputFile == "-" {
2966
target = dockerCli.Out()
@@ -32,13 +69,25 @@ func mergeCmd(dockerCli command.Cli) *cobra.Command {
3269
if err != nil {
3370
return err
3471
}
35-
defer target.(io.WriteCloser).Close()
3672
}
37-
return packager.Merge(appname, target)
73+
if err := packager.Merge(extractedApp.AppName, target); err != nil {
74+
return err
75+
}
76+
if mergeOutputFile != "-" {
77+
// Need to close for the Rename to work on windows.
78+
target.(io.WriteCloser).Close()
79+
}
80+
if inPlace {
81+
if err := os.RemoveAll(extractedApp.OriginalAppName); err != nil {
82+
return errors.Wrap(err, "failed to erase previous application")
83+
}
84+
if err := os.Rename(mergeOutputFile, extractedApp.OriginalAppName); err != nil {
85+
return errors.Wrap(err, "failed to rename new application")
86+
}
87+
}
88+
return nil
3889
},
3990
}
40-
if internal.Experimental == "on" {
41-
cmd.Flags().StringVarP(&mergeOutputFile, "output", "o", "-", "Output file (default: stdout)")
42-
}
91+
cmd.Flags().StringVarP(&mergeOutputFile, "output", "o", "", "Output file (default: in-place)")
4392
return cmd
4493
}

cmd/docker-app/root.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,20 +38,20 @@ func addCommands(cmd *cobra.Command, dockerCli *command.DockerCli) {
3838
initCmd(),
3939
inspectCmd(dockerCli),
4040
lsCmd(),
41+
mergeCmd(dockerCli),
4142
pushCmd(),
4243
renderCmd(dockerCli),
4344
saveCmd(dockerCli),
45+
splitCmd(),
4446
versionCmd(dockerCli),
4547
)
4648
if internal.Experimental == "on" {
4749
cmd.AddCommand(
4850
imageAddCmd(),
4951
imageLoadCmd(),
5052
loadCmd(),
51-
mergeCmd(dockerCli),
5253
packCmd(dockerCli),
5354
pullCmd(),
54-
splitCmd(),
5555
unpackCmd(),
5656
)
5757
}

cmd/docker-app/split.go

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,45 @@
11
package main
22

33
import (
4-
"github.com/docker/app/internal"
4+
"os"
5+
56
"github.com/docker/app/internal/packager"
67
"github.com/docker/cli/cli"
8+
"github.com/pkg/errors"
79
"github.com/spf13/cobra"
810
)
911

1012
var splitOutputDir string
1113

1214
func splitCmd() *cobra.Command {
1315
cmd := &cobra.Command{
14-
Use: "split [<app-name>] [-o output_dir]",
16+
Use: "split [<app-name>] [-o output]",
1517
Short: "Split a single-file application into multiple files",
1618
Args: cli.RequiresMaxArgs(1),
1719
RunE: func(cmd *cobra.Command, args []string) error {
18-
appname, cleanup, err := packager.Extract(firstOrEmpty(args))
20+
extractedApp, err := packager.ExtractWithOrigin(firstOrEmpty(args))
1921
if err != nil {
2022
return err
2123
}
22-
defer cleanup()
23-
return packager.Split(appname, splitOutputDir)
24+
defer extractedApp.Cleanup()
25+
inPlace := splitOutputDir == ""
26+
if inPlace {
27+
splitOutputDir = extractedApp.OriginalAppName + ".tmp"
28+
}
29+
if err := packager.Split(extractedApp.AppName, splitOutputDir); err != nil {
30+
return err
31+
}
32+
if inPlace {
33+
if err := os.RemoveAll(extractedApp.OriginalAppName); err != nil {
34+
return errors.Wrap(err, "failed to erase previous application directory")
35+
}
36+
if err := os.Rename(splitOutputDir, extractedApp.OriginalAppName); err != nil {
37+
return errors.Wrap(err, "failed to rename new application directory")
38+
}
39+
}
40+
return nil
2441
},
2542
}
26-
if internal.Experimental == "on" {
27-
cmd.Flags().StringVarP(&splitOutputDir, "output", "o", ".", "Output directory")
28-
}
43+
cmd.Flags().StringVarP(&splitOutputDir, "output", "o", "", "Output application directory (default: in-place)")
2944
return cmd
3045
}

e2e/binary_test.go

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -303,19 +303,19 @@ func TestHelmInvalidStackVersionBinary(t *testing.T) {
303303
}
304304

305305
func TestSplitMergeBinary(t *testing.T) {
306-
dockerApp, hasExperimental := getBinary(t)
307-
if !hasExperimental {
308-
t.Skip("experimental mode needed for this test")
309-
}
306+
dockerApp, _ := getBinary(t)
310307
app := "render/envvariables"
311308
assertCommand(t, dockerApp, "merge", app, "-o", "remerged.dockerapp")
312309
defer os.Remove("remerged.dockerapp")
313310
// test that inspect works on single-file
314311
assertCommandOutput(t, "envvariables-inspect.golden", dockerApp, "inspect", "remerged")
315312
// split it
316-
assertCommand(t, dockerApp, "split", "remerged", "-o", "splitted.dockerapp")
317-
defer os.RemoveAll("splitted.dockerapp")
318-
assertCommandOutput(t, "envvariables-inspect.golden", dockerApp, "inspect", "splitted")
313+
assertCommand(t, dockerApp, "split", "remerged", "-o", "split.dockerapp")
314+
defer os.RemoveAll("split.dockerapp")
315+
assertCommandOutput(t, "envvariables-inspect.golden", dockerApp, "inspect", "split")
316+
// test inplace
317+
assertCommand(t, dockerApp, "merge", "split")
318+
assertCommand(t, dockerApp, "split", "split")
319319
}
320320

321321
func TestImageBinary(t *testing.T) {

internal/packager/extract.go

Lines changed: 32 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,13 @@ import (
1414
"github.com/pkg/errors"
1515
)
1616

17+
// ExtractedApp represents a potentially extracted application package
18+
type ExtractedApp struct {
19+
OriginalAppName string
20+
AppName string
21+
Cleanup func()
22+
}
23+
1724
var (
1825
noop = func() {}
1926
)
@@ -47,7 +54,7 @@ func findApp() (string, error) {
4754
}
4855

4956
// extractImage extracts a docker application in a docker image to a temporary directory
50-
func extractImage(appname string) (string, func(), error) {
57+
func extractImage(appname string) (ExtractedApp, error) {
5158
var imagename string
5259
if strings.Contains(appname, ":") {
5360
nametag := strings.Split(appname, ":")
@@ -65,42 +72,49 @@ func extractImage(appname string) (string, func(), error) {
6572
}
6673
tempDir, err := ioutil.TempDir("", "dockerapp")
6774
if err != nil {
68-
return "", noop, errors.Wrap(err, "failed to create temporary directory")
75+
return ExtractedApp{}, errors.Wrap(err, "failed to create temporary directory")
6976
}
7077
defer os.RemoveAll(tempDir)
7178
err = Load(imagename, tempDir)
7279
if err != nil {
7380
if !strings.Contains(imagename, "/") {
74-
return "", noop, fmt.Errorf("could not locate application in either filesystem or docker image")
81+
return ExtractedApp{}, fmt.Errorf("could not locate application in either filesystem or docker image")
7582
}
7683
// Try to pull it
7784
cmd := exec.Command("docker", "pull", imagename)
7885
if err := cmd.Run(); err != nil {
79-
return "", noop, fmt.Errorf("could not locate application in filesystem, docker image or registry")
86+
return ExtractedApp{}, fmt.Errorf("could not locate application in filesystem, docker image or registry")
8087
}
8188
if err := Load(imagename, tempDir); err != nil {
82-
return "", noop, errors.Wrap(err, "failed to load pulled image")
89+
return ExtractedApp{}, errors.Wrap(err, "failed to load pulled image")
8390
}
8491
}
8592
// this gave us a compressed app, run through extract again
86-
return Extract(filepath.Join(tempDir, appname))
93+
appname, cleanup, err := Extract(filepath.Join(tempDir, appname))
94+
return ExtractedApp{"", appname, cleanup}, err
95+
}
96+
97+
// Extract extracts the app content if it's an archive or single-file
98+
func Extract(appname string) (string, func(), error) {
99+
extracted, err := ExtractWithOrigin(appname)
100+
return extracted.AppName, extracted.Cleanup, err
87101
}
88102

89-
// Extract extracts the app content if argument is an archive, or does nothing if a dir.
90-
// It returns effective app name, and cleanup function
103+
// ExtractWithOrigin extracts the app content if argument is an archive, or does nothing if a dir.
104+
// It returns source file, effective app name, and cleanup function
91105
// If appname is empty, it looks into cwd, and all subdirs for a single matching .dockerapp
92106
// If nothing is found, it looks for an image and loads it
93-
func Extract(appname string) (string, func(), error) {
107+
func ExtractWithOrigin(appname string) (ExtractedApp, error) {
94108
if appname == "" {
95109
var err error
96110
if appname, err = findApp(); err != nil {
97-
return "", nil, err
111+
return ExtractedApp{}, err
98112
}
99113
}
100114
if appname == "." {
101115
var err error
102116
if appname, err = os.Getwd(); err != nil {
103-
return "", nil, errors.Wrap(err, "cannot resolve current working directory")
117+
return ExtractedApp{}, errors.Wrap(err, "cannot resolve current working directory")
104118
}
105119
}
106120
originalAppname := appname
@@ -118,12 +132,12 @@ func Extract(appname string) (string, func(), error) {
118132
}
119133
if s.IsDir() {
120134
// directory: already decompressed
121-
return appname, noop, nil
135+
return ExtractedApp{appname, appname, noop}, nil
122136
}
123137
// not a dir: single-file or a tarball package, extract that in a temp dir
124138
tempDir, err := ioutil.TempDir("", "dockerapp")
125139
if err != nil {
126-
return "", noop, errors.Wrap(err, "failed to create temporary directory")
140+
return ExtractedApp{}, errors.Wrap(err, "failed to create temporary directory")
127141
}
128142
defer func() {
129143
if err != nil {
@@ -132,16 +146,16 @@ func Extract(appname string) (string, func(), error) {
132146
}()
133147
appDir := filepath.Join(tempDir, filepath.Base(appname))
134148
if err = os.Mkdir(appDir, 0755); err != nil {
135-
return "", noop, errors.Wrap(err, "failed to create application in temporary directory")
149+
return ExtractedApp{}, errors.Wrap(err, "failed to create application in temporary directory")
136150
}
137151
if err = extract(appname, appDir); err == nil {
138-
return appDir, func() { os.RemoveAll(tempDir) }, nil
152+
return ExtractedApp{appname, appDir, func() { os.RemoveAll(tempDir) }}, nil
139153
}
140154
if err = extractSingleFile(appname, appDir); err != nil {
141-
return "", noop, err
155+
return ExtractedApp{}, err
142156
}
143157
// not a tarball, single-file then
144-
return appDir, func() { os.RemoveAll(tempDir) }, nil
158+
return ExtractedApp{appname, appDir, func() { os.RemoveAll(tempDir) }}, nil
145159
}
146160

147161
func extractSingleFile(appname, appDir string) error {
@@ -177,6 +191,7 @@ func extract(appname, outputDir string) error {
177191
if err != nil {
178192
return errors.Wrap(err, "failed to open application package")
179193
}
194+
defer f.Close()
180195
tarReader := tar.NewReader(f)
181196
outputDir = outputDir + "/"
182197
for {

0 commit comments

Comments
 (0)