@@ -3,12 +3,18 @@ package main
33import (
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\t CREATED\t CREATED BY\t SIZE\t COMMENT\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+ }
0 commit comments