Skip to content

Commit 4f24278

Browse files
committed
merge branch 'cmd-umoci-stat'
Implements cyphar/umoci#38 Signed-off-by: Aleksa Sarai <[email protected]>
2 parents 08a5586 + 0d1bca7 commit 4f24278

File tree

25 files changed

+1203
-647
lines changed

25 files changed

+1203
-647
lines changed

cmd/umoci/main.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ func main() {
8080
gcCommand,
8181
createCommand,
8282
tagCommand,
83+
statCommand,
8384
}
8485

8586
// Actually run umoci.

cmd/umoci/stat.go

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
/*
2+
* umoci: Umoci Modifies Open Containers' Images
3+
* Copyright (C) 2016 SUSE LLC.
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
package main
19+
20+
import (
21+
"encoding/json"
22+
"fmt"
23+
"os"
24+
25+
"github.com/cyphar/umoci/image/cas"
26+
"github.com/opencontainers/image-spec/specs-go/v1"
27+
"github.com/urfave/cli"
28+
"golang.org/x/net/context"
29+
)
30+
31+
var statCommand = cli.Command{
32+
Name: "stat",
33+
Usage: "displays status information of an image manifest",
34+
ArgsUsage: `--image <image-path> --tag <reference>
35+
36+
Where "<image-path>" is the path to the OCI image, and "<reference>" is the
37+
name of the reference descriptor to stat.
38+
39+
WARNING: Do not depend on the output of this tool unless you're using --json.
40+
The intention of the default formatting of this tool is that it is easy for
41+
humans to read, and might change in future versions.`,
42+
43+
Flags: []cli.Flag{
44+
// FIXME: This really should be a global option.
45+
cli.StringFlag{
46+
Name: "image",
47+
Usage: "path to OCI image bundle",
48+
},
49+
cli.StringFlag{
50+
Name: "tag",
51+
Usage: "reference descriptor name to stat",
52+
},
53+
cli.BoolFlag{
54+
Name: "json",
55+
Usage: "output the stat information as a JSON encoded blob",
56+
},
57+
},
58+
59+
Action: stat,
60+
}
61+
62+
func stat(ctx *cli.Context) error {
63+
// FIXME: Is there a nicer way of dealing with mandatory arguments?
64+
imagePath := ctx.String("image")
65+
if imagePath == "" {
66+
return fmt.Errorf("image path cannot be empty")
67+
}
68+
tagName := ctx.String("tag")
69+
if tagName == "" {
70+
return fmt.Errorf("reference name cannot be empty")
71+
}
72+
73+
// Get a reference to the CAS.
74+
engine, err := cas.Open(imagePath)
75+
if err != nil {
76+
return err
77+
}
78+
defer engine.Close()
79+
80+
manifestDescriptor, err := engine.GetReference(context.TODO(), tagName)
81+
if err != nil {
82+
return err
83+
}
84+
85+
// FIXME: Implement support for manifest lists.
86+
if manifestDescriptor.MediaType != v1.MediaTypeImageManifest {
87+
return fmt.Errorf("--from descriptor does not point to v1.MediaTypeImageManifest: not implemented: %s", manifestDescriptor.MediaType)
88+
}
89+
90+
// Get stat information.
91+
ms, err := Stat(context.TODO(), engine, *manifestDescriptor)
92+
if err != nil {
93+
return err
94+
}
95+
96+
// Output the stat information.
97+
if ctx.Bool("json") {
98+
// Use JSON.
99+
if err := json.NewEncoder(os.Stdout).Encode(ms); err != nil {
100+
return err
101+
}
102+
} else {
103+
if err := ms.Format(os.Stdout); err != nil {
104+
return err
105+
}
106+
}
107+
108+
return nil
109+
}

cmd/umoci/utils.go

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,18 @@ package main
33
import (
44
"bytes"
55
"encoding/json"
6+
"fmt"
67
"io"
78
"os"
89
"path/filepath"
10+
"strings"
11+
"text/tabwriter"
912

13+
"github.com/cyphar/umoci/image/cas"
1014
"github.com/cyphar/umoci/image/layer"
15+
"github.com/docker/go-units"
1116
ispec "github.com/opencontainers/image-spec/specs-go/v1"
17+
"golang.org/x/net/context"
1218
)
1319

1420
// FIXME: This should be moved to a library. Too much of this code is in the
@@ -74,3 +80,121 @@ func ReadBundleMeta(bundle string) (UmociMeta, error) {
7480
err = json.NewDecoder(fh).Decode(&meta)
7581
return meta, err
7682
}
83+
84+
// ManifestStat has information about a given OCI manifest.
85+
// TODO: Implement support for manifest lists, this should also be able to
86+
// contain stat information for a list of manifests.
87+
type ManifestStat struct {
88+
// TODO: Flesh this out. Currently it's only really being used to get an
89+
// equivalent of docker-history(1). We really need to add more
90+
// information about it.
91+
92+
// History stores the history information for the manifest.
93+
History []historyStat `json:"history"`
94+
}
95+
96+
// Format formats a ManifestStat using the default formatting, and writes the
97+
// result to the given writer.
98+
// TODO: This should really be implemented in a way that allows for users to
99+
// define their own custom templates for different blocks (meaning that
100+
// this should use text/template rather than using tabwriters manually.
101+
func (ms ManifestStat) Format(w io.Writer) error {
102+
// Output history information.
103+
tw := tabwriter.NewWriter(w, 4, 2, 1, ' ', 0)
104+
fmt.Fprintf(tw, "LAYER\tCREATED\tCREATED BY\tSIZE\tCOMMENT\n")
105+
for _, histEntry := range ms.History {
106+
var (
107+
created = strings.Replace(histEntry.Created, "\t", " ", -1)
108+
createdBy = strings.Replace(histEntry.CreatedBy, "\t", " ", -1)
109+
comment = strings.Replace(histEntry.Comment, "\t", " ", -1)
110+
layerID = "<none>"
111+
size = "<none>"
112+
)
113+
114+
if !histEntry.EmptyLayer {
115+
layerID = histEntry.Layer.Digest
116+
size = units.HumanSize(float64(histEntry.Layer.Size))
117+
}
118+
119+
// TODO: We need to truncate some of the fields.
120+
121+
fmt.Fprintf(tw, "%s\t%s\t%s\t%s\t%s\n", layerID, created, createdBy, size, comment)
122+
}
123+
tw.Flush()
124+
return nil
125+
}
126+
127+
// historyStat contains information about a single entry in the history of a
128+
// manifest. This is essentially equivalent to a single record from
129+
// docker-history(1).
130+
type historyStat struct {
131+
// Layer is the descriptor referencing where the layer is stored. If it is
132+
// nil, then this entry is an empty_layer (and thus doesn't have a backing
133+
// diff layer).
134+
Layer *ispec.Descriptor `json:"layer"`
135+
136+
// DiffID is an additional piece of information to Layer. It stores the
137+
// DiffID of the given layer corresponding to the history entry. If DiffID
138+
// is "", then this entry is an empty_layer.
139+
DiffID string `json:"diff_id"`
140+
141+
// History is embedded in the stat information.
142+
ispec.History
143+
}
144+
145+
// Stat computes the ManifestStat for a given manifest blob. The provided
146+
// descriptor must refer to an OCI Manifest.
147+
func Stat(ctx context.Context, engine cas.Engine, manifestDescriptor ispec.Descriptor) (ManifestStat, error) {
148+
var stat ManifestStat
149+
150+
if manifestDescriptor.MediaType != ispec.MediaTypeImageManifest {
151+
return stat, fmt.Errorf("stat: cannot stat a non-manifest descriptor: invalid media type '%s'", manifestDescriptor.MediaType)
152+
}
153+
154+
// We have to get the actual manifest.
155+
manifestBlob, err := cas.FromDescriptor(ctx, engine, &manifestDescriptor)
156+
if err != nil {
157+
return stat, err
158+
}
159+
manifest, ok := manifestBlob.Data.(*ispec.Manifest)
160+
if !ok {
161+
return stat, fmt.Errorf("stat: cannot convert manifestBlob to manifest")
162+
}
163+
164+
// Now get the config.
165+
configBlob, err := cas.FromDescriptor(ctx, engine, &manifest.Config)
166+
if err != nil {
167+
return stat, err
168+
}
169+
config, ok := configBlob.Data.(*ispec.Image)
170+
if !ok {
171+
return stat, fmt.Errorf("stat: cannot convert configBlob to config")
172+
}
173+
174+
// TODO: This should probably be moved into separate functions.
175+
176+
// Generate the history of the image. Because the config.History entries
177+
// are in the same order as the manifest.Layer entries this is fairly
178+
// simple. However, we only increment the layer index if a layer was
179+
// actually generated by a history entry.
180+
layerIdx := 0
181+
for _, histEntry := range config.History {
182+
info := historyStat{
183+
History: histEntry,
184+
DiffID: "",
185+
Layer: nil,
186+
}
187+
188+
// Only fill the other information and increment layerIdx if it's a
189+
// non-empty layer.
190+
if !histEntry.EmptyLayer {
191+
info.DiffID = config.RootFS.DiffIDs[layerIdx]
192+
info.Layer = &manifest.Layers[layerIdx]
193+
layerIdx++
194+
}
195+
196+
stat.History = append(stat.History, info)
197+
}
198+
199+
return stat, nil
200+
}

glide.lock

Lines changed: 11 additions & 8 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

glide.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package: umoci
22
import:
3+
- package: github.com/docker/go-units
4+
version: ~0.3.1
35
- package: github.com/Sirupsen/logrus
46
version: ~0.11.0
57
- package: github.com/urfave/cli

test/stat.bats

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
#!/usr/bin/env bats -t
2+
# umoci: Umoci Modifies Open Containers' Images
3+
# Copyright (C) 2016 SUSE LLC.
4+
#
5+
# Licensed under the Apache License, Version 2.0 (the "License");
6+
# you may not use this file except in compliance with the License.
7+
# You may obtain a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing, software
12+
# distributed under the License is distributed on an "AS IS" BASIS,
13+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
# See the License for the specific language governing permissions and
15+
# limitations under the License.
16+
17+
load helpers
18+
19+
function setup() {
20+
setup_image
21+
}
22+
23+
function teardown() {
24+
teardown_image
25+
}
26+
27+
@test "umoci stat --json" {
28+
# Make sure that stat looks about right.
29+
umoci stat --image "$IMAGE" --tag "$TAG" --json
30+
[ "$status" -eq 0 ]
31+
32+
statFile="$(mktemp --tmpdir="$BATS_TMPDIR" umoci-stat.XXXXXX)"
33+
echo "$output" > "$statFile"
34+
35+
# .history should have at least one entry.
36+
sane_run jq -SMr '.history | length' "$statFile"
37+
[ "$status" -eq 0 ]
38+
[ "$output" -ge 1 ]
39+
40+
# There should be at least one non-empty_layer.
41+
sane_run jq -SMr '[.history[] | .empty_layer == false] | any' "$statFile"
42+
[ "$status" -eq 0 ]
43+
[[ "$output" == "true" ]]
44+
}
45+
46+
# TODO: Add a test to make sure that empty_layer and layer are mutually
47+
# exclusive. Unfortunately, jq doesn't provide an XOR operator...

0 commit comments

Comments
 (0)