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

Commit 1d919ae

Browse files
committed
Add command to create a tag from an application image
`docker app image tag SOURCE_APP_IMAGE[:TAG] TARGET_APP_IMAGE[:TAG]` Signed-off-by: Yves Brissaud <[email protected]>
1 parent 32af0c3 commit 1d919ae

File tree

4 files changed

+290
-12
lines changed

4 files changed

+290
-12
lines changed

e2e/images_test.go

Lines changed: 109 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,7 @@ import (
1212
)
1313

1414
var (
15-
reg = regexp.MustCompile("Digest is (.*).")
16-
expected = `APP IMAGE APP NAME
17-
%s push-pull
18-
a-simple-app:latest simple
19-
b-simple-app:latest simple
20-
`
15+
reg = regexp.MustCompile("Digest is (.*).")
2116
)
2217

2318
func insertBundles(t *testing.T, cmd icmd.Cmd, dir *fs.Dir, info dindSwarmAndRegistryInfo) string {
@@ -42,6 +37,12 @@ func insertBundles(t *testing.T, cmd icmd.Cmd, dir *fs.Dir, info dindSwarmAndReg
4237
return digest
4338
}
4439

40+
func expectImageListOutput(t *testing.T, cmd icmd.Cmd, output string) {
41+
cmd.Command = dockerCli.Command("app", "image", "ls")
42+
result := icmd.RunCmd(cmd).Assert(t, icmd.Success)
43+
assert.Equal(t, result.Stdout(), output)
44+
}
45+
4546
func TestImageList(t *testing.T) {
4647
runWithDindSwarmAndRegistry(t, func(info dindSwarmAndRegistryInfo) {
4748
cmd := info.configuredCmd
@@ -50,10 +51,13 @@ func TestImageList(t *testing.T) {
5051

5152
digest := insertBundles(t, cmd, dir, info)
5253

54+
expected := `APP IMAGE APP NAME
55+
%s push-pull
56+
a-simple-app:latest simple
57+
b-simple-app:latest simple
58+
`
5359
expectedOutput := fmt.Sprintf(expected, info.registryAddress+"/c-myapp@"+digest)
54-
cmd.Command = dockerCli.Command("app", "image", "ls")
55-
result := icmd.RunCmd(cmd).Assert(t, icmd.Success)
56-
assert.Equal(t, result.Stdout(), expectedOutput)
60+
expectImageListOutput(t, cmd, expectedOutput)
5761
})
5862
}
5963

@@ -85,8 +89,101 @@ Deleted: b-simple-app:latest`,
8589
})
8690

8791
expectedOutput := "APP IMAGE APP NAME\n"
88-
cmd.Command = dockerCli.Command("app", "image", "ls")
89-
result := icmd.RunCmd(cmd).Assert(t, icmd.Success)
90-
assert.Equal(t, result.Stdout(), expectedOutput)
92+
expectImageListOutput(t, cmd, expectedOutput)
93+
})
94+
}
95+
96+
func TestImageTag(t *testing.T) {
97+
runWithDindSwarmAndRegistry(t, func(info dindSwarmAndRegistryInfo) {
98+
cmd := info.configuredCmd
99+
dir := fs.NewDir(t, "")
100+
defer dir.Remove()
101+
102+
// given a first available image
103+
cmd.Command = dockerCli.Command("app", "bundle", filepath.Join("testdata", "simple", "simple.dockerapp"), "--tag", "a-simple-app", "--output", dir.Join("simple-bundle.json"))
104+
icmd.RunCmd(cmd).Assert(t, icmd.Success)
105+
106+
singleImageExpectation := `APP IMAGE APP NAME
107+
a-simple-app:latest simple
108+
`
109+
expectImageListOutput(t, cmd, singleImageExpectation)
110+
111+
// with no argument
112+
cmd.Command = dockerCli.Command("app", "bundle", "tag")
113+
icmd.RunCmd(cmd).Assert(t, icmd.Expected{ExitCode: 1})
114+
115+
// with one argument
116+
cmd.Command = dockerCli.Command("app", "bundle", "tag", "a-simple-app")
117+
icmd.RunCmd(cmd).Assert(t, icmd.Expected{ExitCode: 1})
118+
119+
// with invalid src reference
120+
cmd.Command = dockerCli.Command("app", "bundle", "tag", "a-simple-app$2", "b-simple-app")
121+
icmd.RunCmd(cmd).Assert(t, icmd.Expected{ExitCode: 1})
122+
123+
// with invalid target reference
124+
cmd.Command = dockerCli.Command("app", "bundle", "tag", "a-simple-app", "b@simple-app")
125+
icmd.RunCmd(cmd).Assert(t, icmd.Expected{ExitCode: 1})
126+
127+
// tag image with only names
128+
cmd.Command = dockerCli.Command("app", "image", "tag", "a-simple-app", "b-simple-app")
129+
icmd.RunCmd(cmd).Assert(t, icmd.Success)
130+
expectImageListOutput(t, cmd, `APP IMAGE APP NAME
131+
a-simple-app:latest simple
132+
b-simple-app:latest simple
133+
`)
134+
135+
// target tag
136+
cmd.Command = dockerCli.Command("app", "image", "tag", "a-simple-app", "a-simple-app:0.1")
137+
icmd.RunCmd(cmd).Assert(t, icmd.Success)
138+
expectImageListOutput(t, cmd, `APP IMAGE APP NAME
139+
a-simple-app:0.1 simple
140+
a-simple-app:latest simple
141+
b-simple-app:latest simple
142+
`)
143+
144+
// source tag
145+
cmd.Command = dockerCli.Command("app", "image", "tag", "a-simple-app:0.1", "c-simple-app")
146+
icmd.RunCmd(cmd).Assert(t, icmd.Success)
147+
expectImageListOutput(t, cmd, `APP IMAGE APP NAME
148+
a-simple-app:0.1 simple
149+
a-simple-app:latest simple
150+
b-simple-app:latest simple
151+
c-simple-app:latest simple
152+
`)
153+
154+
// source and target tags
155+
cmd.Command = dockerCli.Command("app", "image", "tag", "a-simple-app:0.1", "b-simple-app:0.2")
156+
icmd.RunCmd(cmd).Assert(t, icmd.Success)
157+
expectImageListOutput(t, cmd, `APP IMAGE APP NAME
158+
a-simple-app:0.1 simple
159+
a-simple-app:latest simple
160+
b-simple-app:0.2 simple
161+
b-simple-app:latest simple
162+
c-simple-app:latest simple
163+
`)
164+
165+
// given a new application
166+
cmd.Command = dockerCli.Command("app", "bundle", filepath.Join("testdata", "push-pull", "push-pull.dockerapp"), "--tag", "push-pull", "--output", dir.Join("push-pull-bundle.json"))
167+
icmd.RunCmd(cmd).Assert(t, icmd.Success)
168+
expectImageListOutput(t, cmd, `APP IMAGE APP NAME
169+
a-simple-app:0.1 simple
170+
a-simple-app:latest simple
171+
b-simple-app:0.2 simple
172+
b-simple-app:latest simple
173+
c-simple-app:latest simple
174+
push-pull:latest push-pull
175+
`)
176+
177+
// can be tagged to an existing tag
178+
cmd.Command = dockerCli.Command("app", "image", "tag", "push-pull", "b-simple-app:0.2")
179+
icmd.RunCmd(cmd).Assert(t, icmd.Success)
180+
expectImageListOutput(t, cmd, `APP IMAGE APP NAME
181+
a-simple-app:0.1 simple
182+
a-simple-app:latest simple
183+
b-simple-app:0.2 push-pull
184+
b-simple-app:latest simple
185+
c-simple-app:latest simple
186+
push-pull:latest push-pull
187+
`)
91188
})
92189
}

internal/commands/image/command.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ func Cmd(dockerCli command.Cli) *cobra.Command {
1515
cmd.AddCommand(
1616
listCmd(dockerCli),
1717
rmCmd(),
18+
tagCmd(),
1819
)
1920

2021
return cmd

internal/commands/image/tag.go

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
package image
2+
3+
import (
4+
"fmt"
5+
6+
"github.com/deislabs/cnab-go/bundle"
7+
"github.com/docker/app/internal/store"
8+
"github.com/docker/cli/cli"
9+
"github.com/docker/cli/cli/config"
10+
"github.com/docker/distribution/reference"
11+
"github.com/spf13/cobra"
12+
)
13+
14+
func tagCmd() *cobra.Command {
15+
cmd := &cobra.Command{
16+
Short: "Create a new tag from an application image",
17+
Use: "tag SOURCE_APP_IMAGE[:TAG] TARGET_APP_IMAGE[:TAG]",
18+
Args: cli.ExactArgs(2),
19+
RunE: func(cmd *cobra.Command, args []string) error {
20+
appstore, err := store.NewApplicationStore(config.Dir())
21+
if err != nil {
22+
return err
23+
}
24+
25+
bundleStore, err := appstore.BundleStore()
26+
if err != nil {
27+
return err
28+
}
29+
30+
return runTag(bundleStore, args[0], args[1])
31+
},
32+
}
33+
34+
return cmd
35+
}
36+
37+
func runTag(bundleStore store.BundleStore, srcAppImage, destAppImage string) error {
38+
srcRef, err := readBundle(srcAppImage, bundleStore)
39+
if err != nil {
40+
return err
41+
}
42+
43+
return storeBundle(srcRef, destAppImage, bundleStore)
44+
}
45+
46+
func readBundle(name string, bundleStore store.BundleStore) (*bundle.Bundle, error) {
47+
cnabRef, err := stringToRef(name)
48+
if err != nil {
49+
return nil, err
50+
}
51+
52+
return bundleStore.Read(cnabRef)
53+
}
54+
55+
func storeBundle(bundle *bundle.Bundle, name string, bundleStore store.BundleStore) error {
56+
cnabRef, err := stringToRef(name)
57+
if err != nil {
58+
return err
59+
}
60+
61+
return bundleStore.Store(cnabRef, bundle)
62+
}
63+
64+
func stringToRef(name string) (reference.Named, error) {
65+
cnabRef, err := reference.ParseNormalizedNamed(name)
66+
if err != nil {
67+
return nil, fmt.Errorf("could not parse '%s' as a valid reference: %v", name, err)
68+
}
69+
70+
return reference.TagNameOnly(cnabRef), nil
71+
}

internal/commands/image/tag_test.go

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
package image
2+
3+
import (
4+
"fmt"
5+
"testing"
6+
7+
"gotest.tools/assert"
8+
9+
"github.com/deislabs/cnab-go/bundle"
10+
"github.com/docker/cli/cli/config/configfile"
11+
"github.com/docker/distribution/reference"
12+
)
13+
14+
type bundleStoreStub struct {
15+
ReadBundle *bundle.Bundle
16+
ReadError error
17+
StoredBundle string
18+
StoredError error
19+
}
20+
21+
func (b *bundleStoreStub) Store(ref reference.Named, bndle *bundle.Bundle) error {
22+
defer func() {
23+
b.StoredError = nil
24+
}()
25+
26+
b.StoredBundle = ref.String()
27+
28+
return b.StoredError
29+
}
30+
31+
func (b *bundleStoreStub) Read(ref reference.Named) (*bundle.Bundle, error) {
32+
defer func() {
33+
b.ReadBundle = nil
34+
b.ReadError = nil
35+
}()
36+
return b.ReadBundle, b.ReadError
37+
}
38+
39+
func (b *bundleStoreStub) List() ([]reference.Named, error) {
40+
return nil, nil
41+
}
42+
43+
func (b *bundleStoreStub) Remove(ref reference.Named) error {
44+
return nil
45+
}
46+
47+
func (b *bundleStoreStub) LookupOrPullBundle(ref reference.Named, pullRef bool, config *configfile.ConfigFile, insecureRegistries []string) (*bundle.Bundle, error) {
48+
return nil, nil
49+
}
50+
51+
var mockedBundleStore = &bundleStoreStub{}
52+
53+
func TestInvalidSourceReference(t *testing.T) {
54+
// given a bad source image reference
55+
const badRef = "b@d reference"
56+
57+
err := runTag(mockedBundleStore, badRef, "")
58+
59+
assert.ErrorContains(t, err, fmt.Sprintf("could not parse '%s' as a valid reference", badRef))
60+
}
61+
62+
func TestUnexistingSource(t *testing.T) {
63+
// given a well formatted source image reference
64+
const unexistingRef = "unexisting"
65+
// and given bundle store will return an error on Read
66+
mockedBundleStore.ReadError = fmt.Errorf("error from bundleStore.Read")
67+
68+
err := runTag(mockedBundleStore, unexistingRef, "dest")
69+
70+
assert.Assert(t, err != nil)
71+
}
72+
73+
func TestInvalidDestinationReference(t *testing.T) {
74+
// given a bundle is returned by bundleStore.Read
75+
mockedBundleStore.ReadBundle = &bundle.Bundle{}
76+
// and given a bad destination reference
77+
const badRef = "b@d reference"
78+
79+
err := runTag(mockedBundleStore, "ref", badRef)
80+
81+
assert.ErrorContains(t, err, fmt.Sprintf("could not parse '%s' as a valid reference", badRef))
82+
}
83+
84+
func TestBundleNotStored(t *testing.T) {
85+
// given a bundle is returned by bundleStore.Read
86+
mockedBundleStore.ReadBundle = &bundle.Bundle{}
87+
// and given bundleStore.Store will return an error
88+
mockedBundleStore.StoredError = fmt.Errorf("error from bundleStore.Store")
89+
90+
err := runTag(mockedBundleStore, "src-app", "dest-app")
91+
92+
assert.Assert(t, err != nil)
93+
}
94+
95+
func TestSuccessfulyTag(t *testing.T) {
96+
// given a bundle is returned by bundleStore.Read
97+
mockedBundleStore.ReadBundle = &bundle.Bundle{}
98+
// and given valid source and output references
99+
const (
100+
srcRef = "src-app"
101+
destRef = "dest-app"
102+
normalizedDestRef = "docker.io/library/dest-app:latest"
103+
)
104+
105+
err := runTag(mockedBundleStore, srcRef, destRef)
106+
107+
assert.NilError(t, err)
108+
assert.Equal(t, mockedBundleStore.StoredBundle, normalizedDestRef)
109+
}

0 commit comments

Comments
 (0)