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

Commit 368989a

Browse files
authored
Merge pull request #520 from silvin-lubecki/bundle-with-tag
Bundle command now saves the bundle to the bundle store
2 parents 8aacca2 + 4722969 commit 368989a

File tree

6 files changed

+265
-43
lines changed

6 files changed

+265
-43
lines changed

e2e/commands_test.go

Lines changed: 54 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -261,35 +261,60 @@ func TestBundle(t *testing.T) {
261261
cmd.Command = dockerCli.Command("load", "-i", tmpDir.Join("cnab-app-base.tar.gz"))
262262
icmd.RunCmd(cmd).Assert(t, icmd.Success)
263263

264-
// Bundle the docker application package to a CNAB bundle, using the build-context.
265-
cmd.Command = dockerCli.Command("app", "bundle", filepath.Join("testdata", "simple", "simple.dockerapp"), "--output", tmpDir.Join("bundle.json"))
266-
icmd.RunCmd(cmd).Assert(t, icmd.Success)
267-
268-
// Check the resulting CNAB bundle.json
269-
golden.Assert(t, string(golden.Get(t, tmpDir.Join("bundle.json"))), "simple-bundle.json.golden")
270-
271-
// List the images on the build context daemon and checks the invocation image is there
272-
cmd.Command = dockerCli.Command("image", "ls", "--format", "{{.Repository}}:{{.Tag}}")
273-
icmd.RunCmd(cmd).Assert(t, icmd.Expected{ExitCode: 0, Out: "simple:1.1.0-beta1-invoc"})
274-
275-
// Copy all the files from the invocation image and check them
276-
cmd.Command = dockerCli.Command("create", "--name", "invocation", "simple:1.1.0-beta1-invoc")
277-
id := strings.TrimSpace(icmd.RunCmd(cmd).Assert(t, icmd.Success).Stdout())
278-
cmd.Command = dockerCli.Command("cp", "invocation:/cnab/app/simple.dockerapp", tmpDir.Join("simple.dockerapp"))
279-
icmd.RunCmd(cmd).Assert(t, icmd.Success)
280-
cmd.Command = dockerCli.Command("rm", "--force", id)
281-
icmd.RunCmd(cmd).Assert(t, icmd.Success)
282-
283-
appDir := filepath.Join("testdata", "simple", "simple.dockerapp")
284-
manifest := fs.Expected(
285-
t,
286-
fs.WithMode(0755),
287-
fs.WithFile(internal.MetadataFileName, readFile(t, filepath.Join(appDir, internal.MetadataFileName)), fs.WithMode(0644)),
288-
fs.WithFile(internal.ComposeFileName, readFile(t, filepath.Join(appDir, internal.ComposeFileName)), fs.WithMode(0644)),
289-
fs.WithFile(internal.ParametersFileName, readFile(t, filepath.Join(appDir, internal.ParametersFileName)), fs.WithMode(0644)),
290-
)
291-
292-
assert.Assert(t, fs.Equal(tmpDir.Join("simple.dockerapp"), manifest))
264+
testCases := []struct {
265+
name string
266+
cmd []string
267+
invocImage string
268+
expectedBundle string
269+
}{
270+
{
271+
name: "simple-bundle",
272+
cmd: dockerCli.Command("app", "bundle", filepath.Join("testdata", "simple", "simple.dockerapp"), "--output", tmpDir.Join("simple-bundle.json")),
273+
invocImage: "simple:1.1.0-beta1-invoc",
274+
expectedBundle: "simple-bundle.json.golden",
275+
},
276+
{
277+
name: "bundle-with-tag",
278+
cmd: dockerCli.Command("app", "bundle", filepath.Join("testdata", "simple", "simple.dockerapp"), "--tag", "myimage:mytag", "--output", tmpDir.Join("bundle-with-tag.json")),
279+
invocImage: "myimage:mytag-invoc",
280+
expectedBundle: "bundle-with-tag.json.golden",
281+
},
282+
}
283+
for _, tc := range testCases {
284+
t.Run(tc.name, func(t *testing.T) {
285+
testDir := fs.NewDir(t, "")
286+
defer testDir.Remove()
287+
288+
// Bundle the docker application package to a CNAB bundle, using the build-context.
289+
cmd.Command = tc.cmd
290+
icmd.RunCmd(cmd).Assert(t, icmd.Success)
291+
292+
// Check the resulting CNAB bundle.json
293+
golden.Assert(t, string(golden.Get(t, tmpDir.Join(tc.name+".json"))), tc.expectedBundle)
294+
295+
// List the images on the build context daemon and checks the invocation image is there
296+
cmd.Command = dockerCli.Command("image", "ls", "--format", "{{.Repository}}:{{.Tag}}")
297+
icmd.RunCmd(cmd).Assert(t, icmd.Expected{ExitCode: 0, Out: tc.invocImage})
298+
299+
// Copy all the files from the invocation image and check them
300+
cmd.Command = dockerCli.Command("create", "--name", "invocation", tc.invocImage)
301+
id := strings.TrimSpace(icmd.RunCmd(cmd).Assert(t, icmd.Success).Stdout())
302+
cmd.Command = dockerCli.Command("cp", "invocation:/cnab/app/simple.dockerapp", testDir.Join("simple.dockerapp"))
303+
icmd.RunCmd(cmd).Assert(t, icmd.Success)
304+
cmd.Command = dockerCli.Command("rm", "--force", id)
305+
icmd.RunCmd(cmd).Assert(t, icmd.Success)
306+
307+
appDir := filepath.Join("testdata", "simple", "simple.dockerapp")
308+
manifest := fs.Expected(
309+
t,
310+
fs.WithMode(0755),
311+
fs.WithFile(internal.MetadataFileName, readFile(t, filepath.Join(appDir, internal.MetadataFileName)), fs.WithMode(0644)),
312+
fs.WithFile(internal.ComposeFileName, readFile(t, filepath.Join(appDir, internal.ComposeFileName)), fs.WithMode(0644)),
313+
fs.WithFile(internal.ParametersFileName, readFile(t, filepath.Join(appDir, internal.ParametersFileName)), fs.WithMode(0644)),
314+
)
315+
assert.Assert(t, fs.Equal(testDir.Join("simple.dockerapp"), manifest))
316+
})
317+
}
293318
}
294319

295320
func TestDockerAppLifecycle(t *testing.T) {
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
{
2+
"name": "simple",
3+
"version": "1.1.0-beta1",
4+
"description": "new fancy webapp with microservices",
5+
"maintainers": [
6+
{
7+
"name": "John Developer",
8+
"email": "[email protected]"
9+
},
10+
{
11+
"name": "Jane Developer",
12+
"email": "[email protected]"
13+
}
14+
],
15+
"invocationImages": [
16+
{
17+
"imageType": "docker",
18+
"image": "myimage:mytag-invoc"
19+
}
20+
],
21+
"images": {
22+
"api": {
23+
"imageType": "docker",
24+
"image": "python:3.6",
25+
"description": "python:3.6"
26+
},
27+
"db": {
28+
"imageType": "docker",
29+
"image": "postgres:9.3",
30+
"description": "postgres:9.3"
31+
},
32+
"web": {
33+
"imageType": "docker",
34+
"image": "nginx:latest",
35+
"description": "nginx:latest"
36+
}
37+
},
38+
"actions": {
39+
"com.docker.app.inspect": {
40+
"stateless": true
41+
},
42+
"com.docker.app.render": {
43+
"stateless": true
44+
},
45+
"com.docker.app.status": {}
46+
},
47+
"parameters": {
48+
"api_host": {
49+
"type": "string",
50+
"defaultValue": "example.com",
51+
"destination": {
52+
"env": "docker_param1"
53+
}
54+
},
55+
"com.docker.app.kubernetes-namespace": {
56+
"type": "string",
57+
"defaultValue": "",
58+
"metadata": {
59+
"description": "Namespace in which to deploy"
60+
},
61+
"destination": {
62+
"env": "DOCKER_KUBERNETES_NAMESPACE"
63+
},
64+
"apply-to": [
65+
"install",
66+
"upgrade",
67+
"uninstall",
68+
"com.docker.app.status"
69+
]
70+
},
71+
"com.docker.app.orchestrator": {
72+
"type": "string",
73+
"defaultValue": "",
74+
"allowedValues": [
75+
"",
76+
"swarm",
77+
"kubernetes"
78+
],
79+
"metadata": {
80+
"description": "Orchestrator on which to deploy"
81+
},
82+
"destination": {
83+
"env": "DOCKER_STACK_ORCHESTRATOR"
84+
},
85+
"apply-to": [
86+
"install",
87+
"upgrade",
88+
"uninstall",
89+
"com.docker.app.status"
90+
]
91+
},
92+
"com.docker.app.render-format": {
93+
"type": "string",
94+
"defaultValue": "yaml",
95+
"allowedValues": [
96+
"yaml",
97+
"json"
98+
],
99+
"metadata": {
100+
"description": "Output format for the render command"
101+
},
102+
"destination": {
103+
"env": "DOCKER_RENDER_FORMAT"
104+
},
105+
"apply-to": [
106+
"com.docker.app.render"
107+
]
108+
},
109+
"com.docker.app.share-registry-creds": {
110+
"type": "bool",
111+
"defaultValue": false,
112+
"metadata": {
113+
"description": "Share registry credentials with the invocation image"
114+
},
115+
"destination": {
116+
"env": "DOCKER_SHARE_REGISTRY_CREDS"
117+
}
118+
},
119+
"static_subdir": {
120+
"type": "string",
121+
"defaultValue": "data/static",
122+
"destination": {
123+
"env": "docker_param2"
124+
}
125+
},
126+
"web_port": {
127+
"type": "string",
128+
"defaultValue": "8082",
129+
"destination": {
130+
"env": "docker_param3"
131+
}
132+
}
133+
},
134+
"credentials": {
135+
"com.docker.app.registry-creds": {
136+
"path": "/cnab/app/registry-creds.json"
137+
},
138+
"docker.context": {
139+
"path": "/cnab/app/context.dockercontext"
140+
}
141+
}
142+
}

internal/commands/bundle.go

Lines changed: 55 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,12 @@ import (
99

1010
"github.com/deislabs/cnab-go/bundle"
1111
"github.com/docker/app/internal/packager"
12+
"github.com/docker/app/internal/store"
1213
"github.com/docker/app/types"
1314
"github.com/docker/app/types/metadata"
1415
"github.com/docker/cli/cli"
1516
"github.com/docker/cli/cli/command"
17+
"github.com/docker/cli/cli/config"
1618
"github.com/docker/distribution/reference"
1719
dockertypes "github.com/docker/docker/api/types"
1820
"github.com/docker/docker/pkg/jsonmessage"
@@ -22,6 +24,7 @@ import (
2224

2325
type bundleOptions struct {
2426
out string
27+
tag string
2528
}
2629

2730
func bundleCmd(dockerCli command.Cli) *cobra.Command {
@@ -37,17 +40,27 @@ func bundleCmd(dockerCli command.Cli) *cobra.Command {
3740
}
3841

3942
cmd.Flags().StringVarP(&opts.out, "output", "o", "bundle.json", "Output file (- for stdout)")
43+
cmd.Flags().StringVarP(&opts.tag, "tag", "t", "", "Name and optionally a tag in the 'name:tag' format")
4044
return cmd
4145
}
4246

4347
func runBundle(dockerCli command.Cli, appName string, opts bundleOptions) error {
44-
bundle, err := makeBundle(dockerCli, appName)
48+
ref, err := getNamedTagged(opts.tag)
49+
if err != nil {
50+
return err
51+
}
52+
53+
bundle, err := makeBundle(dockerCli, appName, ref)
4554
if err != nil {
4655
return err
4756
}
4857
if bundle == nil || len(bundle.InvocationImages) == 0 {
4958
return fmt.Errorf("failed to create bundle %q", appName)
5059
}
60+
if err := persistInBundleStore(ref, bundle); err != nil {
61+
return err
62+
}
63+
5164
fmt.Fprintf(dockerCli.Out(), "Invocation image %q successfully built\n", bundle.InvocationImages[0].Image)
5265
bundleBytes, err := json.MarshalIndent(bundle, "", "\t")
5366
if err != nil {
@@ -60,18 +73,18 @@ func runBundle(dockerCli command.Cli, appName string, opts bundleOptions) error
6073
return ioutil.WriteFile(opts.out, bundleBytes, 0644)
6174
}
6275

63-
func makeBundle(dockerCli command.Cli, appName string) (*bundle.Bundle, error) {
76+
func makeBundle(dockerCli command.Cli, appName string, refOverride reference.NamedTagged) (*bundle.Bundle, error) {
6477
app, err := packager.Extract(appName)
6578
if err != nil {
6679
return nil, err
6780
}
6881
defer app.Cleanup()
69-
return makeBundleFromApp(dockerCli, app)
82+
return makeBundleFromApp(dockerCli, app, refOverride)
7083
}
7184

72-
func makeBundleFromApp(dockerCli command.Cli, app *types.App) (*bundle.Bundle, error) {
85+
func makeBundleFromApp(dockerCli command.Cli, app *types.App, refOverride reference.NamedTagged) (*bundle.Bundle, error) {
7386
meta := app.Metadata()
74-
invocationImageName, err := makeInvocationImageName(meta)
87+
invocationImageName, err := makeInvocationImageName(meta, refOverride)
7588
if err != nil {
7689
return nil, err
7790
}
@@ -101,14 +114,47 @@ func makeBundleFromApp(dockerCli command.Cli, app *types.App) (*bundle.Bundle, e
101114
return packager.ToCNAB(app, invocationImageName)
102115
}
103116

104-
func makeInvocationImageName(meta metadata.AppMetadata) (string, error) {
105-
return makeCNABImageName(meta, "-invoc")
117+
func makeInvocationImageName(meta metadata.AppMetadata, refOverride reference.NamedTagged) (string, error) {
118+
if refOverride != nil {
119+
return makeCNABImageName(reference.FamiliarName(refOverride), refOverride.Tag(), "-invoc")
120+
}
121+
return makeCNABImageName(meta.Name, meta.Version, "-invoc")
106122
}
107123

108-
func makeCNABImageName(meta metadata.AppMetadata, suffix string) (string, error) {
109-
name := fmt.Sprintf("%s:%s%s", meta.Name, meta.Version, suffix)
124+
func makeCNABImageName(appName, appVersion, suffix string) (string, error) {
125+
name := fmt.Sprintf("%s:%s%s", appName, appVersion, suffix)
110126
if _, err := reference.ParseNormalizedNamed(name); err != nil {
111127
return "", errors.Wrapf(err, "image name %q is invalid, please check name and version fields", name)
112128
}
113129
return name, nil
114130
}
131+
132+
func persistInBundleStore(ref reference.Named, bndle *bundle.Bundle) error {
133+
if ref == nil {
134+
return nil
135+
}
136+
appstore, err := store.NewApplicationStore(config.Dir())
137+
if err != nil {
138+
return err
139+
}
140+
bundleStore, err := appstore.BundleStore()
141+
if err != nil {
142+
return err
143+
}
144+
return bundleStore.Store(ref, bndle)
145+
}
146+
147+
func getNamedTagged(tag string) (reference.NamedTagged, error) {
148+
if tag == "" {
149+
return nil, nil
150+
}
151+
namedRef, err := reference.ParseNormalizedNamed(tag)
152+
if err != nil {
153+
return nil, err
154+
}
155+
ref, ok := reference.TagNameOnly(namedRef).(reference.NamedTagged)
156+
if !ok {
157+
return nil, fmt.Errorf("tag %q must be name with a tag in the 'name:tag' format", tag)
158+
}
159+
return ref, nil
160+
}

internal/commands/bundle_test.go

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ func TestMakeInvocationImage(t *testing.T) {
1111
testcases := []struct {
1212
name string
1313
meta metadata.AppMetadata
14+
tag string
1415
expected string
1516
err string
1617
}{
@@ -20,14 +21,22 @@ func TestMakeInvocationImage(t *testing.T) {
2021
expected: "name:version-invoc",
2122
},
2223
{
23-
name: "simple-metadata",
24+
name: "tag-override",
25+
meta: metadata.AppMetadata{Name: "name", Version: "version"},
26+
expected: "myimage:mytag-invoc",
27+
tag: "myimage:mytag",
28+
},
29+
{
30+
name: "invalid-metadata",
2431
meta: metadata.AppMetadata{Name: "WrongName&%*", Version: "version"},
2532
err: "invalid",
2633
},
2734
}
2835
for _, c := range testcases {
2936
t.Run(c.name, func(t *testing.T) {
30-
actual, err := makeInvocationImageName(c.meta)
37+
ref, err := getNamedTagged(c.tag)
38+
assert.NilError(t, err)
39+
actual, err := makeInvocationImageName(c.meta, ref)
3140
if c.err != "" {
3241
assert.ErrorContains(t, err, c.err)
3342
assert.Equal(t, actual, "", "On "+c.meta.Name)

0 commit comments

Comments
 (0)