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

Commit 4722969

Browse files
Add --tag flag to the bundle command to persist the bundle in the local bundle store.
Installation or any other command will be able to use this reference. Invocation image is using the same name, appending "-invoc" to the tag. Signed-off-by: Silvin Lubecki <[email protected]>
1 parent 137c8cd commit 4722969

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
@@ -10,10 +10,12 @@ import (
1010
"github.com/deislabs/cnab-go/bundle"
1111
"github.com/docker/app/internal"
1212
"github.com/docker/app/internal/packager"
13+
"github.com/docker/app/internal/store"
1314
"github.com/docker/app/types"
1415
"github.com/docker/app/types/metadata"
1516
"github.com/docker/cli/cli"
1617
"github.com/docker/cli/cli/command"
18+
"github.com/docker/cli/cli/config"
1719
"github.com/docker/distribution/reference"
1820
dockertypes "github.com/docker/docker/api/types"
1921
"github.com/docker/docker/pkg/jsonmessage"
@@ -23,6 +25,7 @@ import (
2325

2426
type bundleOptions struct {
2527
out string
28+
tag string
2629
}
2730

2831
func bundleCmd(dockerCli command.Cli) *cobra.Command {
@@ -38,17 +41,27 @@ func bundleCmd(dockerCli command.Cli) *cobra.Command {
3841
}
3942

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

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

64-
func makeBundle(dockerCli command.Cli, appName string) (*bundle.Bundle, error) {
77+
func makeBundle(dockerCli command.Cli, appName string, refOverride reference.NamedTagged) (*bundle.Bundle, error) {
6578
app, err := packager.Extract(appName)
6679
if err != nil {
6780
return nil, err
6881
}
6982
defer app.Cleanup()
70-
return makeBundleFromApp(dockerCli, app)
83+
return makeBundleFromApp(dockerCli, app, refOverride)
7184
}
7285

73-
func makeBundleFromApp(dockerCli command.Cli, app *types.App) (*bundle.Bundle, error) {
86+
func makeBundleFromApp(dockerCli command.Cli, app *types.App, refOverride reference.NamedTagged) (*bundle.Bundle, error) {
7487
meta := app.Metadata()
75-
invocationImageName, err := makeInvocationImageName(meta)
88+
invocationImageName, err := makeInvocationImageName(meta, refOverride)
7689
if err != nil {
7790
return nil, err
7891
}
@@ -102,14 +115,47 @@ func makeBundleFromApp(dockerCli command.Cli, app *types.App) (*bundle.Bundle, e
102115
return packager.ToCNAB(app, invocationImageName)
103116
}
104117

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

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

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)