github.com/dctrud/umoci@v0.4.3-0.20191016193643-05a1d37de015/utils.go (about) 1 /* 2 * umoci: Umoci Modifies Open Containers' Images 3 * Copyright (C) 2016, 2017, 2018 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 umoci 19 20 import ( 21 "bytes" 22 "encoding/json" 23 "fmt" 24 "io" 25 "os" 26 "path/filepath" 27 "strings" 28 "text/tabwriter" 29 30 "github.com/apex/log" 31 "github.com/docker/go-units" 32 "github.com/openSUSE/umoci/oci/casext" 33 igen "github.com/openSUSE/umoci/oci/config/generate" 34 "github.com/openSUSE/umoci/oci/layer" 35 "github.com/openSUSE/umoci/pkg/idtools" 36 ispec "github.com/opencontainers/image-spec/specs-go/v1" 37 "github.com/pkg/errors" 38 "github.com/urfave/cli" 39 "github.com/vbatts/go-mtree" 40 "golang.org/x/net/context" 41 ) 42 43 // FIXME: This should be moved to a library. Too much of this code is in the 44 // cmd/... code, but should really be refactored to the point where it 45 // can be useful to other people. This is _particularly_ true for the 46 // code which repacks images (the changes to the config, manifest and 47 // CAS should be made into a library). 48 49 // MtreeKeywords is the set of keywords used by umoci for verification and diff 50 // generation of a bundle. This is based on mtree.DefaultKeywords, but is 51 // hardcoded here to ensure that vendor changes don't mess things up. 52 var MtreeKeywords = []mtree.Keyword{ 53 "size", 54 "type", 55 "uid", 56 "gid", 57 "mode", 58 "link", 59 "nlink", 60 "tar_time", 61 "sha256digest", 62 "xattr", 63 } 64 65 // MetaName is the name of umoci's metadata file that is stored in all 66 // bundles extracted by umoci. 67 const MetaName = "umoci.json" 68 69 // MetaVersion is the version of Meta supported by this code. The 70 // value is only bumped for updates which are not backwards compatible. 71 const MetaVersion = "2" 72 73 // Meta represents metadata about how umoci unpacked an image to a bundle 74 // and other similar information. It is used to keep track of information that 75 // is required when repacking an image and other similar bundle information. 76 type Meta struct { 77 // Version is the version of umoci used to unpack the bundle. This is used 78 // to future-proof the umoci.json information. 79 Version string `json:"umoci_version"` 80 81 // From is a copy of the descriptor pointing to the image manifest that was 82 // used to unpack the bundle. Essentially it's a resolved form of the 83 // --image argument to umoci-unpack(1). 84 From casext.DescriptorPath `json:"from_descriptor_path"` 85 86 // MapOptions is the parsed version of --uid-map, --gid-map and --rootless 87 // arguments to umoci-unpack(1). While all of these options technically do 88 // not need to be the same for corresponding umoci-unpack(1) and 89 // umoci-repack(1) calls, changing them is not recommended and so the 90 // default should be that they are the same. 91 MapOptions layer.MapOptions `json:"map_options"` 92 } 93 94 // WriteTo writes a JSON-serialised version of Meta to the given io.Writer. 95 func (m Meta) WriteTo(w io.Writer) (int64, error) { 96 buf := new(bytes.Buffer) 97 err := json.NewEncoder(io.MultiWriter(buf, w)).Encode(m) 98 return int64(buf.Len()), err 99 } 100 101 // WriteBundleMeta writes an umoci.json file to the given bundle path. 102 func WriteBundleMeta(bundle string, meta Meta) error { 103 fh, err := os.Create(filepath.Join(bundle, MetaName)) 104 if err != nil { 105 return errors.Wrap(err, "create metadata") 106 } 107 defer fh.Close() 108 109 _, err = meta.WriteTo(fh) 110 return errors.Wrap(err, "write metadata") 111 } 112 113 // ReadBundleMeta reads and parses the umoci.json file from a given bundle path. 114 func ReadBundleMeta(bundle string) (Meta, error) { 115 var meta Meta 116 117 fh, err := os.Open(filepath.Join(bundle, MetaName)) 118 if err != nil { 119 return meta, errors.Wrap(err, "open metadata") 120 } 121 defer fh.Close() 122 123 err = json.NewDecoder(fh).Decode(&meta) 124 if meta.Version != MetaVersion { 125 if err == nil { 126 err = fmt.Errorf("unsupported umoci.json version: %s", meta.Version) 127 } 128 } 129 return meta, errors.Wrap(err, "decode metadata") 130 } 131 132 // ManifestStat has information about a given OCI manifest. 133 // TODO: Implement support for manifest lists, this should also be able to 134 // contain stat information for a list of manifests. 135 type ManifestStat struct { 136 // TODO: Flesh this out. Currently it's only really being used to get an 137 // equivalent of docker-history(1). We really need to add more 138 // information about it. 139 140 // History stores the history information for the manifest. 141 History []historyStat `json:"history"` 142 } 143 144 // Format formats a ManifestStat using the default formatting, and writes the 145 // result to the given writer. 146 // TODO: This should really be implemented in a way that allows for users to 147 // define their own custom templates for different blocks (meaning that 148 // this should use text/template rather than using tabwriters manually. 149 func (ms ManifestStat) Format(w io.Writer) error { 150 // Output history information. 151 tw := tabwriter.NewWriter(w, 4, 2, 1, ' ', 0) 152 fmt.Fprintf(tw, "LAYER\tCREATED\tCREATED BY\tSIZE\tCOMMENT\n") 153 for _, histEntry := range ms.History { 154 var ( 155 created = strings.Replace(histEntry.Created.Format(igen.ISO8601), "\t", " ", -1) 156 createdBy = strings.Replace(histEntry.CreatedBy, "\t", " ", -1) 157 comment = strings.Replace(histEntry.Comment, "\t", " ", -1) 158 layerID = "<none>" 159 size = "<none>" 160 ) 161 162 if !histEntry.EmptyLayer { 163 layerID = histEntry.Layer.Digest.String() 164 size = units.HumanSize(float64(histEntry.Layer.Size)) 165 } 166 167 // TODO: We need to truncate some of the fields. 168 169 fmt.Fprintf(tw, "%s\t%s\t%s\t%s\t%s\n", layerID, created, createdBy, size, comment) 170 } 171 tw.Flush() 172 return nil 173 } 174 175 // historyStat contains information about a single entry in the history of a 176 // manifest. This is essentially equivalent to a single record from 177 // docker-history(1). 178 type historyStat struct { 179 // Layer is the descriptor referencing where the layer is stored. If it is 180 // nil, then this entry is an empty_layer (and thus doesn't have a backing 181 // diff layer). 182 Layer *ispec.Descriptor `json:"layer"` 183 184 // DiffID is an additional piece of information to Layer. It stores the 185 // DiffID of the given layer corresponding to the history entry. If DiffID 186 // is "", then this entry is an empty_layer. 187 DiffID string `json:"diff_id"` 188 189 // History is embedded in the stat information. 190 ispec.History 191 } 192 193 // Stat computes the ManifestStat for a given manifest blob. The provided 194 // descriptor must refer to an OCI Manifest. 195 func Stat(ctx context.Context, engine casext.Engine, manifestDescriptor ispec.Descriptor) (ManifestStat, error) { 196 var stat ManifestStat 197 198 if manifestDescriptor.MediaType != ispec.MediaTypeImageManifest { 199 return stat, errors.Errorf("stat: cannot stat a non-manifest descriptor: invalid media type '%s'", manifestDescriptor.MediaType) 200 } 201 202 // We have to get the actual manifest. 203 manifestBlob, err := engine.FromDescriptor(ctx, manifestDescriptor) 204 if err != nil { 205 return stat, err 206 } 207 manifest, ok := manifestBlob.Data.(ispec.Manifest) 208 if !ok { 209 // Should _never_ be reached. 210 return stat, errors.Errorf("[internal error] unknown manifest blob type: %s", manifestBlob.MediaType) 211 } 212 213 // Now get the config. 214 configBlob, err := engine.FromDescriptor(ctx, manifest.Config) 215 if err != nil { 216 return stat, errors.Wrap(err, "stat") 217 } 218 config, ok := configBlob.Data.(ispec.Image) 219 if !ok { 220 // Should _never_ be reached. 221 return stat, errors.Errorf("[internal error] unknown config blob type: %s", configBlob.MediaType) 222 } 223 224 // TODO: This should probably be moved into separate functions. 225 226 // Generate the history of the image. Because the config.History entries 227 // are in the same order as the manifest.Layer entries this is fairly 228 // simple. However, we only increment the layer index if a layer was 229 // actually generated by a history entry. 230 layerIdx := 0 231 for _, histEntry := range config.History { 232 info := historyStat{ 233 History: histEntry, 234 DiffID: "", 235 Layer: nil, 236 } 237 238 // Only fill the other information and increment layerIdx if it's a 239 // non-empty layer. 240 if !histEntry.EmptyLayer { 241 info.DiffID = config.RootFS.DiffIDs[layerIdx].String() 242 info.Layer = &manifest.Layers[layerIdx] 243 layerIdx++ 244 } 245 246 stat.History = append(stat.History, info) 247 } 248 249 return stat, nil 250 } 251 252 // GenerateBundleManifest creates and writes an mtree of the rootfs in the given 253 // bundle path, using the supplied fsEval method 254 func GenerateBundleManifest(mtreeName string, bundlePath string, fsEval mtree.FsEval) error { 255 mtreePath := filepath.Join(bundlePath, mtreeName+".mtree") 256 fullRootfsPath := filepath.Join(bundlePath, layer.RootfsName) 257 258 log.WithFields(log.Fields{ 259 "keywords": MtreeKeywords, 260 "mtree": mtreePath, 261 }).Debugf("umoci: generating mtree manifest") 262 263 log.Info("computing filesystem manifest ...") 264 dh, err := mtree.Walk(fullRootfsPath, nil, MtreeKeywords, fsEval) 265 if err != nil { 266 return errors.Wrap(err, "generate mtree spec") 267 } 268 log.Info("... done") 269 270 flags := os.O_CREATE | os.O_WRONLY | os.O_EXCL 271 fh, err := os.OpenFile(mtreePath, flags, 0644) 272 if err != nil { 273 return errors.Wrap(err, "open mtree") 274 } 275 defer fh.Close() 276 277 log.Debugf("umoci: saving mtree manifest") 278 279 if _, err := dh.WriteTo(fh); err != nil { 280 return errors.Wrap(err, "write mtree") 281 } 282 283 return nil 284 } 285 286 // ParseIdmapOptions sets up the mapping options for Meta, using 287 // the arguments specified on the command line 288 func ParseIdmapOptions(meta *Meta, ctx *cli.Context) error { 289 // We need to set mappings if we're in rootless mode. 290 meta.MapOptions.Rootless = ctx.Bool("rootless") 291 if meta.MapOptions.Rootless { 292 if !ctx.IsSet("uid-map") { 293 ctx.Set("uid-map", fmt.Sprintf("0:%d:1", os.Geteuid())) 294 } 295 if !ctx.IsSet("gid-map") { 296 ctx.Set("gid-map", fmt.Sprintf("0:%d:1", os.Getegid())) 297 } 298 } 299 300 for _, uidmap := range ctx.StringSlice("uid-map") { 301 idMap, err := idtools.ParseMapping(uidmap) 302 if err != nil { 303 return errors.Wrapf(err, "failure parsing --uid-map %s", uidmap) 304 } 305 meta.MapOptions.UIDMappings = append(meta.MapOptions.UIDMappings, idMap) 306 } 307 for _, gidmap := range ctx.StringSlice("gid-map") { 308 idMap, err := idtools.ParseMapping(gidmap) 309 if err != nil { 310 return errors.Wrapf(err, "failure parsing --gid-map %s", gidmap) 311 } 312 meta.MapOptions.GIDMappings = append(meta.MapOptions.GIDMappings, idMap) 313 } 314 315 log.WithFields(log.Fields{ 316 "map.uid": meta.MapOptions.UIDMappings, 317 "map.gid": meta.MapOptions.GIDMappings, 318 }).Debugf("parsed mappings") 319 320 return nil 321 }