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

Commit 64ae00f

Browse files
committed
Adjust the "docker app build" UX
docker app build -f <path to x.dockerapp folder> <path to docker context, typically "."> Signed-off-by: Nicolas De Loof <[email protected]>
1 parent 35fdf49 commit 64ae00f

File tree

8 files changed

+87
-39
lines changed

8 files changed

+87
-39
lines changed

e2e/build_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ func TestBuild(t *testing.T) {
1717
cmd := info.configuredCmd
1818

1919
testDir := path.Join("testdata", "build")
20-
cmd.Command = dockerCli.Command("app", "build", path.Join(testDir, "single"), "--tag", "single:1.0.0")
20+
cmd.Command = dockerCli.Command("app", "build", "--tag", "single:1.0.0", "-f", path.Join(testDir, "single.dockerapp"), testDir)
2121
icmd.RunCmd(cmd).Assert(t, icmd.Success)
2222

2323
cfg := getDockerConfigDir(t, cmd)
@@ -42,7 +42,7 @@ func TestBuildWithoutTag(t *testing.T) {
4242
cmd := info.configuredCmd
4343

4444
testDir := path.Join("testdata", "build")
45-
cmd.Command = dockerCli.Command("app", "build", path.Join(testDir, "single"))
45+
cmd.Command = dockerCli.Command("app", "build", "-f", path.Join(testDir, "single.dockerapp"), testDir)
4646
icmd.RunCmd(cmd).Assert(t, icmd.Success)
4747

4848
cfg := getDockerConfigDir(t, cmd)

e2e/commands_test.go

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -74,8 +74,9 @@ func TestRenderFormatters(t *testing.T) {
7474
runWithDindSwarmAndRegistry(t, func(info dindSwarmAndRegistryInfo) {
7575
cmd := info.configuredCmd
7676

77-
appPath := filepath.Join("testdata", "simple", "simple.dockerapp")
78-
cmd.Command = dockerCli.Command("app", "build", appPath, "--tag", "a-simple-tag")
77+
contextPath := filepath.Join("testdata", "simple")
78+
appPath := filepath.Join(contextPath, "simple.dockerapp")
79+
cmd.Command = dockerCli.Command("app", "build", "--tag", "a-simple-tag", contextPath)
7980
icmd.RunCmd(cmd).Assert(t, icmd.Success)
8081

8182
cmd.Command = dockerCli.Command("app", "render", "--formatter", "json", appPath)
@@ -164,7 +165,8 @@ func TestInspectApp(t *testing.T) {
164165
Err: `"docker app inspect" requires exactly 1 argument.`,
165166
})
166167

167-
cmd.Command = dockerCli.Command("app", "build", filepath.Join("testdata", "simple", "simple.dockerapp"), "--tag", "simple-app:1.0.0")
168+
contextPath := filepath.Join("testdata", "simple")
169+
cmd.Command = dockerCli.Command("app", "build", "--tag", "simple-app:1.0.0", contextPath)
168170
cmd.Dir = ""
169171
icmd.RunCmd(cmd).Assert(t, icmd.Success)
170172

e2e/images_test.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,11 @@ import (
1212

1313
func insertBundles(t *testing.T, cmd icmd.Cmd, info dindSwarmAndRegistryInfo) {
1414
// Push an application so that we can later pull it by digest
15-
cmd.Command = dockerCli.Command("app", "build", filepath.Join("testdata", "push-pull", "push-pull.dockerapp"), "--tag", info.registryAddress+"/c-myapp")
15+
cmd.Command = dockerCli.Command("app", "build", "--tag", info.registryAddress+"/c-myapp", filepath.Join("testdata", "push-pull"))
1616
icmd.RunCmd(cmd).Assert(t, icmd.Success)
17-
cmd.Command = dockerCli.Command("app", "build", filepath.Join("testdata", "simple", "simple.dockerapp"), "--tag", "b-simple-app")
17+
cmd.Command = dockerCli.Command("app", "build", "--tag", "b-simple-app", filepath.Join("testdata", "simple"))
1818
icmd.RunCmd(cmd).Assert(t, icmd.Success)
19-
cmd.Command = dockerCli.Command("app", "build", filepath.Join("testdata", "simple", "simple.dockerapp"), "--tag", "a-simple-app")
19+
cmd.Command = dockerCli.Command("app", "build", "--tag", "a-simple-app", filepath.Join("testdata", "simple"))
2020
icmd.RunCmd(cmd).Assert(t, icmd.Success)
2121
}
2222

@@ -86,7 +86,7 @@ func TestImageTag(t *testing.T) {
8686
}
8787

8888
// given a first available image
89-
cmd.Command = dockerCli.Command("app", "build", filepath.Join("testdata", "simple", "simple.dockerapp"), "--tag", "a-simple-app")
89+
cmd.Command = dockerCli.Command("app", "build", "--tag", "a-simple-app", filepath.Join("testdata", "simple"))
9090
icmd.RunCmd(cmd).Assert(t, icmd.Success)
9191

9292
singleImageExpectation := `APP IMAGE APP NAME
@@ -175,7 +175,7 @@ c-simple-app:latest simple
175175
`)
176176

177177
// given a new application
178-
cmd.Command = dockerCli.Command("app", "build", filepath.Join("testdata", "push-pull", "push-pull.dockerapp"), "--tag", "push-pull")
178+
cmd.Command = dockerCli.Command("app", "build", "--tag", "push-pull", filepath.Join("testdata", "push-pull"))
179179
icmd.RunCmd(cmd).Assert(t, icmd.Success)
180180
expectImageListOutput(t, cmd, `APP IMAGE APP NAME
181181
a-simple-app:0.1 simple

e2e/pushpull_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -196,7 +196,7 @@ func TestPushInstallBundle(t *testing.T) {
196196
ref := info.registryAddress + "/test/push-bundle"
197197

198198
// render the app to a bundle, we use the app from the push pull test above.
199-
cmd.Command = dockerCli.Command("app", "build", filepath.Join("testdata", "push-pull", "push-pull.dockerapp"), "--tag", "a-simple-app:1.0.0")
199+
cmd.Command = dockerCli.Command("app", "build", "--tag", "a-simple-app:1.0.0", filepath.Join("testdata", "push-pull"))
200200
icmd.RunCmd(cmd).Assert(t, icmd.Success)
201201

202202
// push it and install to check it is available
@@ -244,7 +244,7 @@ func TestPushInstallBundle(t *testing.T) {
244244
cmdIsolatedStore.Env = append(cmdIsolatedStore.Env, "DOCKER_CONTEXT=swarm-context")
245245

246246
// bundle the app again but this time with a tag to store it into the bundle store
247-
cmdIsolatedStore.Command = dockerCli.Command("app", "build", filepath.Join("testdata", "push-pull", "push-pull.dockerapp"), "--tag", ref2)
247+
cmdIsolatedStore.Command = dockerCli.Command("app", "build", "--tag", ref2, filepath.Join("testdata", "push-pull"))
248248
icmd.RunCmd(cmdIsolatedStore).Assert(t, icmd.Success)
249249
// Push the app without tagging it explicitly
250250
cmdIsolatedStore.Command = dockerCli.Command("app", "push", ref2)

internal/commands/build/build.go

Lines changed: 47 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@ import (
66
"encoding/json"
77
"errors"
88
"fmt"
9+
"io/ioutil"
910
"os"
11+
"path/filepath"
1012
"strconv"
1113
"strings"
1214

@@ -34,14 +36,15 @@ type buildOptions struct {
3436
progress string
3537
pull bool
3638
tag string
39+
folder string
3740
}
3841

3942
func Cmd(dockerCli command.Cli) *cobra.Command {
4043
var opts buildOptions
4144
cmd := &cobra.Command{
42-
Use: "build [APP_NAME] [OPTIONS]",
45+
Use: "build [OPTIONS] [CONTEXT_PATH]",
4346
Short: "Build service images for the application",
44-
Example: `$ docker app build myapp.dockerapp --tag my/app:1.0.0`,
47+
Example: `$ docker app build --tag my/app:1.0.0 .`,
4548
Args: cli.ExactArgs(1),
4649
RunE: func(cmd *cobra.Command, args []string) error {
4750
ref, err := runBuild(dockerCli, args[0], opts)
@@ -56,12 +59,13 @@ func Cmd(dockerCli command.Cli) *cobra.Command {
5659
flags.BoolVar(&opts.noCache, "no-cache", false, "Do not use cache when building the image")
5760
flags.StringVar(&opts.progress, "progress", "auto", "Set type of progress output (auto, plain, tty). Use plain to show container output")
5861
flags.StringVarP(&opts.tag, "tag", "t", "", "Application image and optionally a tag in the 'image:tag' format")
62+
flags.StringVarP(&opts.folder, "folder", "f", "", "Docker app folder containing application definition")
5963
flags.BoolVar(&opts.pull, "pull", false, "Always attempt to pull a newer version of the image")
6064

6165
return cmd
6266
}
6367

64-
func runBuild(dockerCli command.Cli, application string, opt buildOptions) (reference.Reference, error) {
68+
func runBuild(dockerCli command.Cli, contextPath string, opt buildOptions) (reference.Reference, error) {
6569
err := checkMinimalEngineVersion(dockerCli)
6670
if err != nil {
6771
return nil, err
@@ -73,18 +77,35 @@ func runBuild(dockerCli command.Cli, application string, opt buildOptions) (refe
7377
return nil, err
7478
}
7579

80+
application, err := getAppFolder(opt, contextPath)
81+
if err != nil {
82+
return nil, err
83+
}
84+
7685
app, err := packager.Extract(application)
7786
if err != nil {
7887
return nil, err
7988
}
8089
defer app.Cleanup()
8190

82-
buildopts, err := parseCompose(app, opt)
91+
serviceTag := ref
92+
if serviceTag == nil {
93+
named, err := reference.WithName(app.Metadata().Name)
94+
if err != nil {
95+
return nil, err
96+
}
97+
serviceTag, err = reference.WithTag(named, app.Metadata().Version)
98+
if err != nil {
99+
return nil, err
100+
}
101+
}
102+
103+
buildopts, err := parseCompose(app, contextPath, opt, serviceTag)
83104
if err != nil {
84105
return nil, err
85106
}
86107

87-
buildopts["com.docker.app.invocation-image"], err = createInvocationImageBuildOptions(dockerCli, app)
108+
buildopts["com.docker.app.invocation-image"], err = createInvocationImageBuildOptions(dockerCli, app, serviceTag)
88109
if err != nil {
89110
return nil, err
90111
}
@@ -126,6 +147,25 @@ func runBuild(dockerCli command.Cli, application string, opt buildOptions) (refe
126147
return packager.PersistInBundleStore(ref, bundle)
127148
}
128149

150+
func getAppFolder(opt buildOptions, contextPath string) (string, error) {
151+
application := opt.folder
152+
if application == "" {
153+
files, err := ioutil.ReadDir(contextPath)
154+
if err != nil {
155+
return "", err
156+
}
157+
for _, f := range files {
158+
if strings.HasSuffix(f.Name(), ".dockerapp") {
159+
if application != "" {
160+
return "", fmt.Errorf("%s contains multiple *.dockerapp folders, use -f option to select the one to build", contextPath)
161+
}
162+
application = filepath.Join(contextPath, f.Name())
163+
}
164+
}
165+
}
166+
return application, nil
167+
}
168+
129169
func checkMinimalEngineVersion(dockerCli command.Cli) error {
130170
info, err := dockerCli.Client().Info(appcontext.Context())
131171
if err != nil {
@@ -165,7 +205,7 @@ func updateBundle(dockerCli command.Cli, bundle *bundle.Bundle, resp map[string]
165205
return nil
166206
}
167207

168-
func createInvocationImageBuildOptions(dockerCli command.Cli, app *types.App) (build.Options, error) {
208+
func createInvocationImageBuildOptions(dockerCli command.Cli, app *types.App, serviceTag reference.Reference) (build.Options, error) {
169209
buildContext := bytes.NewBuffer(nil)
170210
if err := packager.PackInvocationImageContext(dockerCli, app, buildContext); err != nil {
171211
return build.Options{}, err
@@ -176,6 +216,7 @@ func createInvocationImageBuildOptions(dockerCli command.Cli, app *types.App) (b
176216
ContextPath: "-",
177217
},
178218
Session: []session.Attachable{authprovider.NewDockerAuthProvider(os.Stderr)},
219+
Tags: []string{fmt.Sprintf("%s-installer", serviceTag.String())},
179220
}, nil
180221
}
181222

internal/commands/build/compose.go

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
package build
22

33
import (
4+
"errors"
45
"fmt"
56
"path"
67

8+
"github.com/docker/distribution/reference"
9+
710
"github.com/docker/app/types"
811
"github.com/docker/buildx/build"
912
"github.com/docker/cli/cli/compose/loader"
@@ -12,7 +15,7 @@ import (
1215

1316
// parseCompose do parse app compose file and extract buildx Options
1417
// We don't rely on bake's ReadTargets + TargetsToBuildOpt here as we have to skip environment variable interpolation
15-
func parseCompose(app *types.App, options buildOptions) (map[string]build.Options, error) {
18+
func parseCompose(app *types.App, contextPath string, options buildOptions, reference reference.Reference) (map[string]build.Options, error) {
1619
parsed, err := loader.ParseYAML(app.Composes()[0])
1720
if err != nil {
1821
return nil, err
@@ -29,22 +32,19 @@ func parseCompose(app *types.App, options buildOptions) (map[string]build.Option
2932
continue
3033
}
3134

32-
var tags []string
33-
if service.Image != nil && *service.Image != "" {
34-
tags = []string{*service.Image}
35+
if service.Name == "installer" {
36+
return nil, errors.New("'installer' is a reserved service name, please fix your docker-compose.yml file")
3537
}
3638

37-
// FIXME docker app init should update relative paths
38-
// compose file has been copied to x.dockerapp, so the relative path to build context get broken
39-
contextPath := path.Join(app.Path, "..", service.Build.Context)
39+
tags := []string{fmt.Sprintf("%s-%s", reference.String(), service.Name)}
40+
4041
if service.Build.Dockerfile == "" {
4142
service.Build.Dockerfile = "Dockerfile"
4243
}
43-
dockerfile := path.Join(contextPath, service.Build.Dockerfile)
4444
opts[service.Name] = build.Options{
4545
Inputs: build.Inputs{
46-
ContextPath: contextPath,
47-
DockerfilePath: dockerfile,
46+
ContextPath: path.Join(contextPath, service.Build.Context),
47+
DockerfilePath: path.Join(contextPath, service.Build.Context, service.Build.Dockerfile),
4848
},
4949
BuildArgs: flatten(service.Build.Args),
5050
NoCache: options.noCache,

internal/commands/build/compose_test.go

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,18 @@ package build
33
import (
44
"testing"
55

6+
"github.com/docker/distribution/reference"
7+
68
"github.com/docker/app/internal/packager"
79
"github.com/docker/buildx/build"
810
"gotest.tools/assert"
911
)
1012

1113
func Test_parseCompose(t *testing.T) {
14+
15+
tag, err := reference.Parse("test:1.0")
16+
assert.NilError(t, err)
17+
1218
tests := []struct {
1319
name string
1420
service string
@@ -22,7 +28,7 @@ func Test_parseCompose(t *testing.T) {
2228
ContextPath: "testdata/web",
2329
DockerfilePath: "testdata/web/Dockerfile",
2430
},
25-
Tags: []string{"frontend"},
31+
Tags: []string{"test:1.0-web"},
2632
},
2733
},
2834
{
@@ -33,6 +39,7 @@ func Test_parseCompose(t *testing.T) {
3339
ContextPath: "testdata/web",
3440
DockerfilePath: "testdata/web/Dockerfile.custom",
3541
},
42+
Tags: []string{"test:1.0-web"},
3643
},
3744
},
3845
{
@@ -44,16 +51,15 @@ func Test_parseCompose(t *testing.T) {
4451
DockerfilePath: "testdata/web/Dockerfile",
4552
},
4653
BuildArgs: map[string]string{"foo": "bar"},
54+
Tags: []string{"test:1.0-web"},
4755
},
4856
},
4957
}
5058
for _, tt := range tests {
5159
t.Run(tt.name, func(t *testing.T) {
52-
5360
app, err := packager.Extract("testdata/" + tt.name)
5461
assert.NilError(t, err)
55-
56-
got, err := parseCompose(app, buildOptions{})
62+
got, err := parseCompose(app, "testdata", buildOptions{}, tag)
5763
assert.NilError(t, err)
5864
_, ok := got["dontwant"]
5965
assert.Assert(t, !ok, "parseCompose() should have excluded 'dontwant' service")

internal/packager/extract.go

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

1616
// findApp looks for an app in CWD or subdirs
17-
func findApp() (string, error) {
18-
cwd, err := os.Getwd()
19-
if err != nil {
20-
return "", errors.Wrap(err, "cannot resolve current working directory")
21-
}
17+
func FindApp(cwd string) (string, error) {
2218
if strings.HasSuffix(cwd, internal.AppExtension) {
2319
return cwd, nil
2420
}
@@ -47,8 +43,11 @@ func findApp() (string, error) {
4743
// If nothing is found, it looks for an image and loads it
4844
func Extract(name string, ops ...func(*types.App) error) (*types.App, error) {
4945
if name == "" {
50-
var err error
51-
if name, err = findApp(); err != nil {
46+
cwd, err := os.Getwd()
47+
if err != nil {
48+
return nil, errors.Wrap(err, "cannot resolve current working directory")
49+
}
50+
if name, err = FindApp(cwd); err != nil {
5251
return nil, err
5352
}
5453
}

0 commit comments

Comments
 (0)