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

Commit 9fb9b2d

Browse files
committed
Added created date to app images
Added a created date field to the custom payload of the bundle.json. This is packaged as part of the app image at build time. The CREATED column has been added to the `app image ls` output. This displays the time since the app image was built in the same human readable format as `image ls`. As the creation date is dynamic the app image ID will be unique for each build even if nothing else has changed. To adjust the tests for this the golden bundle.json is now checked using regex matching instead of string matching. Simalarly, the e2e tests for `app image ls` are checked with regexes for the CREATED column as the exact value will depend on how long the tests take to run. Signed-off-by: Nick Adcock <[email protected]>
1 parent 4e0989c commit 9fb9b2d

File tree

8 files changed

+253
-66
lines changed

8 files changed

+253
-66
lines changed

e2e/images_test.go

Lines changed: 51 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package e2e
22

33
import (
44
"bufio"
5+
"fmt"
56
"path/filepath"
67
"regexp"
78
"strings"
@@ -23,7 +24,11 @@ func insertBundles(t *testing.T, cmd icmd.Cmd) {
2324

2425
func assertImageListOutput(t *testing.T, cmd icmd.Cmd, expected string) {
2526
result := icmd.RunCmd(cmd).Assert(t, icmd.Success)
26-
match, _ := regexp.MatchString(expected, result.Stdout())
27+
stdout := result.Stdout()
28+
match, _ := regexp.MatchString(expected, stdout)
29+
if !match {
30+
fmt.Println(stdout)
31+
}
2732
assert.Assert(t, match)
2833
}
2934

@@ -46,6 +51,7 @@ func verifyImageIDListOutput(t *testing.T, cmd icmd.Cmd, count int, distinct int
4651
for scanner.Scan() {
4752
lines = append(lines, scanner.Text())
4853
counter[scanner.Text()]++
54+
fmt.Println(scanner.Text())
4955
}
5056
if err := scanner.Err(); err != nil {
5157
assert.Error(t, err, "Verification failed")
@@ -60,10 +66,10 @@ func TestImageList(t *testing.T) {
6066

6167
insertBundles(t, cmd)
6268

63-
expected := `REPOSITORY TAG APP IMAGE ID APP NAME
64-
a-simple-app latest [a-f0-9]{12} simple
65-
b-simple-app latest [a-f0-9]{12} simple
66-
my.registry:5000/c-myapp latest [a-f0-9]{12} push-pull
69+
expected := `REPOSITORY TAG APP IMAGE ID APP NAME CREATED
70+
a-simple-app latest [a-f0-9]{12} simple [La-z0-9 ]+ ago
71+
b-simple-app latest [a-f0-9]{12} simple [La-z0-9 ]+ ago
72+
my.registry:5000/c-myapp latest [a-f0-9]{12} push-pull [La-z0-9 ]+ ago
6773
`
6874
expectImageListOutput(t, cmd, expected)
6975
})
@@ -73,18 +79,18 @@ func TestImageListQuiet(t *testing.T) {
7379
runWithDindSwarmAndRegistry(t, func(info dindSwarmAndRegistryInfo) {
7480
cmd := info.configuredCmd
7581
insertBundles(t, cmd)
76-
verifyImageIDListOutput(t, cmd, 3, 2)
82+
verifyImageIDListOutput(t, cmd, 3, 3)
7783
})
7884
}
7985

8086
func TestImageListDigests(t *testing.T) {
8187
runWithDindSwarmAndRegistry(t, func(info dindSwarmAndRegistryInfo) {
8288
cmd := info.configuredCmd
8389
insertBundles(t, cmd)
84-
expected := `REPOSITORY TAG DIGEST APP IMAGE ID APP NAME
85-
a-simple-app latest <none> [a-f0-9]{12} simple
86-
b-simple-app latest <none> [a-f0-9]{12} simple
87-
my.registry:5000/c-myapp latest <none> [a-f0-9]{12} push-pull
90+
expected := `REPOSITORY TAG DIGEST APP IMAGE ID APP NAME CREATED
91+
a-simple-app latest <none> [a-f0-9]{12} simple [La-z0-9 ]+ ago
92+
b-simple-app latest <none> [a-f0-9]{12} simple [La-z0-9 ]+ ago
93+
my.registry:5000/c-myapp latest <none> [a-f0-9]{12} push-pull [La-z0-9 ]+ ago
8894
`
8995
expectImageListDigestsOutput(t, cmd, expected)
9096
})
@@ -115,7 +121,7 @@ Deleted: b-simple-app:latest`,
115121
Err: `b-simple-app:latest: reference not found`,
116122
})
117123

118-
expectedOutput := "REPOSITORY TAG APP IMAGE ID APP NAME\n"
124+
expectedOutput := "REPOSITORY TAG APP IMAGE ID APP NAME CREATED\n"
119125
expectImageListOutput(t, cmd, expectedOutput)
120126
})
121127
}
@@ -133,8 +139,8 @@ func TestImageTag(t *testing.T) {
133139
cmd.Command = dockerCli.Command("app", "build", "--tag", "a-simple-app", filepath.Join("testdata", "simple"))
134140
icmd.RunCmd(cmd).Assert(t, icmd.Success)
135141

136-
singleImageExpectation := `REPOSITORY TAG APP IMAGE ID APP NAME
137-
a-simple-app latest [a-f0-9]{12} simple
142+
singleImageExpectation := `REPOSITORY TAG APP IMAGE ID APP NAME CREATED
143+
a-simple-app latest [a-f0-9]{12} simple [La-z0-9 ]+ ago
138144
`
139145
expectImageListOutput(t, cmd, singleImageExpectation)
140146

@@ -183,63 +189,63 @@ a-simple-app latest [a-f0-9]{12} simple
183189
// tag image with only names
184190
dockerAppImageTag("a-simple-app", "b-simple-app")
185191
icmd.RunCmd(cmd).Assert(t, icmd.Success)
186-
expectImageListOutput(t, cmd, `REPOSITORY TAG APP IMAGE ID APP NAME
187-
a-simple-app latest [a-f0-9]{12} simple
188-
b-simple-app latest [a-f0-9]{12} simple
192+
expectImageListOutput(t, cmd, `REPOSITORY TAG APP IMAGE ID APP NAME CREATED
193+
a-simple-app latest [a-f0-9]{12} simple [La-z0-9 ]+ ago
194+
b-simple-app latest [a-f0-9]{12} simple [La-z0-9 ]+ ago
189195
`)
190196

191197
// target tag
192198
dockerAppImageTag("a-simple-app", "a-simple-app:0.1")
193199
icmd.RunCmd(cmd).Assert(t, icmd.Success)
194-
expectImageListOutput(t, cmd, `REPOSITORY TAG APP IMAGE ID APP NAME
195-
a-simple-app 0.1 [a-f0-9]{12} simple
196-
a-simple-app latest [a-f0-9]{12} simple
197-
b-simple-app latest [a-f0-9]{12} simple
200+
expectImageListOutput(t, cmd, `REPOSITORY TAG APP IMAGE ID APP NAME CREATED
201+
a-simple-app 0.1 [a-f0-9]{12} simple [La-z0-9 ]+ ago
202+
a-simple-app latest [a-f0-9]{12} simple [La-z0-9 ]+ ago
203+
b-simple-app latest [a-f0-9]{12} simple [La-z0-9 ]+ ago
198204
`)
199205

200206
// source tag
201207
dockerAppImageTag("a-simple-app:0.1", "c-simple-app")
202208
icmd.RunCmd(cmd).Assert(t, icmd.Success)
203-
expectImageListOutput(t, cmd, `REPOSITORY TAG APP IMAGE ID APP NAME
204-
a-simple-app 0.1 [a-f0-9]{12} simple
205-
a-simple-app latest [a-f0-9]{12} simple
206-
b-simple-app latest [a-f0-9]{12} simple
207-
c-simple-app latest [a-f0-9]{12} simple
209+
expectImageListOutput(t, cmd, `REPOSITORY TAG APP IMAGE ID APP NAME CREATED
210+
a-simple-app 0.1 [a-f0-9]{12} simple [La-z0-9 ]+ ago
211+
a-simple-app latest [a-f0-9]{12} simple [La-z0-9 ]+ ago
212+
b-simple-app latest [a-f0-9]{12} simple [La-z0-9 ]+ ago
213+
c-simple-app latest [a-f0-9]{12} simple [La-z0-9 ]+ ago
208214
`)
209215

210216
// source and target tags
211217
dockerAppImageTag("a-simple-app:0.1", "b-simple-app:0.2")
212218
icmd.RunCmd(cmd).Assert(t, icmd.Success)
213-
expectImageListOutput(t, cmd, `REPOSITORY TAG APP IMAGE ID APP NAME
214-
a-simple-app 0.1 [a-f0-9]{12} simple
215-
a-simple-app latest [a-f0-9]{12} simple
216-
b-simple-app 0.2 [a-f0-9]{12} simple
217-
b-simple-app latest [a-f0-9]{12} simple
218-
c-simple-app latest [a-f0-9]{12} simple
219+
expectImageListOutput(t, cmd, `REPOSITORY TAG APP IMAGE ID APP NAME CREATED
220+
a-simple-app 0.1 [a-f0-9]{12} simple [La-z0-9 ]+ ago
221+
a-simple-app latest [a-f0-9]{12} simple [La-z0-9 ]+ ago
222+
b-simple-app 0.2 [a-f0-9]{12} simple [La-z0-9 ]+ ago
223+
b-simple-app latest [a-f0-9]{12} simple [La-z0-9 ]+ ago
224+
c-simple-app latest [a-f0-9]{12} simple [La-z0-9 ]+ ago
219225
`)
220226

221227
// given a new application
222228
cmd.Command = dockerCli.Command("app", "build", "--tag", "push-pull", filepath.Join("testdata", "push-pull"))
223229
icmd.RunCmd(cmd).Assert(t, icmd.Success)
224-
expectImageListOutput(t, cmd, `REPOSITORY TAG APP IMAGE ID APP NAME
225-
a-simple-app 0.1 [a-f0-9]{12} simple
226-
a-simple-app latest [a-f0-9]{12} simple
227-
b-simple-app 0.2 [a-f0-9]{12} simple
228-
b-simple-app latest [a-f0-9]{12} simple
229-
c-simple-app latest [a-f0-9]{12} simple
230-
push-pull latest [a-f0-9]{12} push-pull
230+
expectImageListOutput(t, cmd, `REPOSITORY TAG APP IMAGE ID APP NAME CREATED
231+
a-simple-app 0.1 [a-f0-9]{12} simple [La-z0-9 ]+ ago
232+
a-simple-app latest [a-f0-9]{12} simple [La-z0-9 ]+ ago
233+
b-simple-app 0.2 [a-f0-9]{12} simple [La-z0-9 ]+ ago
234+
b-simple-app latest [a-f0-9]{12} simple [La-z0-9 ]+ ago
235+
c-simple-app latest [a-f0-9]{12} simple [La-z0-9 ]+ ago
236+
push-pull latest [a-f0-9]{12} push-pull [La-z0-9 ]+ ago
231237
`)
232238

233239
// can be tagged to an existing tag
234240
dockerAppImageTag("push-pull", "b-simple-app:0.2")
235241
icmd.RunCmd(cmd).Assert(t, icmd.Success)
236-
expectImageListOutput(t, cmd, `REPOSITORY TAG APP IMAGE ID APP NAME
237-
a-simple-app 0.1 [a-f0-9]{12} simple
238-
a-simple-app latest [a-f0-9]{12} simple
239-
b-simple-app 0.2 [a-f0-9]{12} push-pull
240-
b-simple-app latest [a-f0-9]{12} simple
241-
c-simple-app latest [a-f0-9]{12} simple
242-
push-pull latest [a-f0-9]{12} push-pull
242+
expectImageListOutput(t, cmd, `REPOSITORY TAG APP IMAGE ID APP NAME CREATED
243+
a-simple-app 0.1 [a-f0-9]{12} simple [La-z0-9 ]+ ago
244+
a-simple-app latest [a-f0-9]{12} simple [La-z0-9 ]+ ago
245+
b-simple-app 0.2 [a-f0-9]{12} push-pull [La-z0-9 ]+ ago
246+
b-simple-app latest [a-f0-9]{12} simple [La-z0-9 ]+ ago
247+
c-simple-app latest [a-f0-9]{12} simple [La-z0-9 ]+ ago
248+
push-pull latest [a-f0-9]{12} push-pull [La-z0-9 ]+ ago
243249
`)
244250
})
245251
}

internal/commands/image/list.go

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,16 @@ import (
66
"io"
77
"strings"
88
"text/tabwriter"
9+
"time"
910

11+
"github.com/docker/app/internal/packager"
1012
"github.com/docker/app/internal/relocated"
11-
1213
"github.com/docker/app/internal/store"
1314
"github.com/docker/cli/cli/command"
1415
"github.com/docker/cli/cli/config"
1516
"github.com/docker/distribution/reference"
1617
"github.com/docker/docker/pkg/stringid"
18+
units "github.com/docker/go-units"
1719
"github.com/spf13/cobra"
1820
)
1921

@@ -177,6 +179,16 @@ func getImageListColumns(options imageListOption) []imageListColumn {
177179
imageListColumn{"APP NAME", func(p pkg) string {
178180
return p.bundle.Name
179181
}},
182+
imageListColumn{"CREATED", func(p pkg) string {
183+
payload, err := packager.CustomPayload(p.bundle.Bundle)
184+
if err != nil {
185+
return ""
186+
}
187+
if createdPayload, ok := payload.(packager.CustomPayloadCreated); ok {
188+
return units.HumanDuration(time.Now().UTC().Sub(createdPayload.CreatedTime())) + " ago"
189+
}
190+
return ""
191+
}},
180192
)
181193
return columns
182194
}

internal/commands/image/list_test.go

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -83,19 +83,19 @@ func TestListCmd(t *testing.T) {
8383
}{
8484
{
8585
name: "TestList",
86-
expectedOutput: `REPOSITORY TAG APP IMAGE ID APP NAME
87-
foo/bar <none> 3f825b2d0657 Digested App
88-
foo/bar 1.0 9aae408ee04f Foo App
89-
<none> <none> a855ac937f2e Quiet App
86+
expectedOutput: `REPOSITORY TAG APP IMAGE ID APP NAME CREATED
87+
foo/bar <none> 3f825b2d0657 Digested App
88+
foo/bar 1.0 9aae408ee04f Foo App
89+
<none> <none> a855ac937f2e Quiet App
9090
`,
9191
options: imageListOption{},
9292
},
9393
{
9494
name: "TestListWithDigests",
95-
expectedOutput: `REPOSITORY TAG DIGEST APP IMAGE ID APP NAME
96-
foo/bar <none> sha256:b59492bb814012ca3d2ce0b6728242d96b4af41687cc82166a4b5d7f2d9fb865 3f825b2d0657 Digested App
97-
foo/bar 1.0 <none> 9aae408ee04f Foo App
98-
<none> <none> sha256:a855ac937f2ed375ba4396bbc49c4093e124da933acd2713fb9bc17d7562a087 a855ac937f2e Quiet App
95+
expectedOutput: `REPOSITORY TAG DIGEST APP IMAGE ID APP NAME CREATED
96+
foo/bar <none> sha256:b59492bb814012ca3d2ce0b6728242d96b4af41687cc82166a4b5d7f2d9fb865 3f825b2d0657 Digested App
97+
foo/bar 1.0 <none> 9aae408ee04f Foo App
98+
<none> <none> sha256:a855ac937f2ed375ba4396bbc49c4093e124da933acd2713fb9bc17d7562a087 a855ac937f2e Quiet App
9999
`,
100100
options: imageListOption{digests: true},
101101
},

internal/packager/cnab.go

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,6 @@ const (
1616
CNABVersion1_0_0 = "v1.0.0"
1717
)
1818

19-
// DockerAppCustom contains extension custom data that docker app injects
20-
// in the bundle.
21-
type DockerAppCustom struct {
22-
Version string `json:"version,omitempty"`
23-
Payload json.RawMessage `json:"payload,omitempty"`
24-
}
25-
2619
// DockerAppArgs represent the object passed to the invocation image
2720
// by Docker App.
2821
type DockerAppArgs struct {
@@ -170,11 +163,17 @@ func ToCNAB(app *types.App, invocationImageName string) (*bundle.Bundle, error)
170163
return nil, err
171164
}
172165

166+
payload, err := newCustomPayload()
167+
if err != nil {
168+
return nil, err
169+
}
170+
173171
bndl := &bundle.Bundle{
174172
SchemaVersion: CNABVersion1_0_0,
175173
Custom: map[string]interface{}{
176174
internal.CustomDockerAppName: DockerAppCustom{
177-
Version: internal.Version,
175+
Version: DockerAppCustomVersion1_0_0,
176+
Payload: payload,
178177
},
179178
},
180179
Credentials: map[string]bundle.Credential{

internal/packager/cnab_test.go

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@ package packager
33
import (
44
"encoding/json"
55
"fmt"
6+
"regexp"
67
"testing"
78

8-
"github.com/docker/app/internal"
99
"github.com/docker/app/types"
1010
"gotest.tools/assert"
1111
"gotest.tools/golden"
@@ -19,6 +19,9 @@ func TestToCNAB(t *testing.T) {
1919
actualJSON, err := json.MarshalIndent(actual, "", " ")
2020
assert.NilError(t, err)
2121
s := golden.Get(t, "bundle-json.golden")
22-
expected := fmt.Sprintf(string(s), internal.Version)
23-
assert.Equal(t, string(actualJSON), expected)
22+
expectedLiteral := regexp.QuoteMeta(string(s))
23+
expected := fmt.Sprintf(expectedLiteral, DockerAppCustomVersionCurrent, `\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d+Z`)
24+
matches, err := regexp.Match(expected, actualJSON)
25+
assert.NilError(t, err)
26+
assert.Assert(t, matches)
2427
}

internal/packager/custom.go

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
package packager
2+
3+
import (
4+
"encoding/json"
5+
"time"
6+
7+
"github.com/deislabs/cnab-go/bundle"
8+
"github.com/docker/app/internal"
9+
)
10+
11+
const (
12+
// DockerAppCustomVersion1_0_0 is the custom payload version 1.0.0
13+
DockerAppCustomVersion1_0_0 = "1.0.0"
14+
15+
// DockerAppCustomVersionCurrent the current payload version
16+
DockerAppCustomVersionCurrent = DockerAppCustomVersion1_0_0
17+
)
18+
19+
// DockerAppCustom contains extension custom data that docker app injects
20+
// in the bundle.
21+
type DockerAppCustom struct {
22+
Version string `json:"version,omitempty"`
23+
Payload json.RawMessage `json:"payload,omitempty"`
24+
}
25+
26+
// CustomPayloadCreated is a custom payload with a created time
27+
type CustomPayloadCreated interface {
28+
CreatedTime() time.Time
29+
}
30+
31+
type payloadV1_0 struct {
32+
Created time.Time `json:"created"`
33+
}
34+
35+
func (p payloadV1_0) CreatedTime() time.Time {
36+
return p.Created
37+
}
38+
39+
func newCustomPayload() (json.RawMessage, error) {
40+
p := payloadV1_0{Created: time.Now().UTC()}
41+
j, err := json.Marshal(&p)
42+
if err != nil {
43+
return nil, err
44+
}
45+
return j, nil
46+
}
47+
48+
// CustomPayload parses and returns the bundle's custom payload
49+
func CustomPayload(b *bundle.Bundle) (interface{}, error) {
50+
custom, err := parseCustomPayload(b)
51+
if err != nil {
52+
return nil, err
53+
}
54+
55+
switch version := custom.Version; version {
56+
case DockerAppCustomVersion1_0_0:
57+
var payload payloadV1_0
58+
if err := json.Unmarshal(custom.Payload, &payload); err != nil {
59+
return nil, err
60+
}
61+
return payload, nil
62+
default:
63+
return nil, nil
64+
}
65+
}
66+
67+
func parseCustomPayload(b *bundle.Bundle) (DockerAppCustom, error) {
68+
customMap, ok := b.Custom[internal.CustomDockerAppName]
69+
if !ok {
70+
return DockerAppCustom{}, nil
71+
}
72+
73+
customJSON, err := json.Marshal(customMap)
74+
if err != nil {
75+
return DockerAppCustom{}, err
76+
}
77+
78+
var custom DockerAppCustom
79+
if err = json.Unmarshal(customJSON, &custom); err != nil {
80+
return DockerAppCustom{}, err
81+
}
82+
83+
return custom, nil
84+
}

0 commit comments

Comments
 (0)