github.com/opencontainers/umoci@v0.4.8-0.20240508124516-656e4836fb0d/utils.go (about) 1 /* 2 * umoci: Umoci Modifies Open Containers' Images 3 * Copyright (C) 2016-2020 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 "context" 23 "encoding/json" 24 "fmt" 25 "io" 26 "os" 27 "path/filepath" 28 "strings" 29 "text/tabwriter" 30 31 "github.com/apex/log" 32 "github.com/docker/go-units" 33 ispec "github.com/opencontainers/image-spec/specs-go/v1" 34 "github.com/opencontainers/umoci/oci/casext" 35 igen "github.com/opencontainers/umoci/oci/config/generate" 36 "github.com/opencontainers/umoci/oci/layer" 37 "github.com/opencontainers/umoci/pkg/idtools" 38 "github.com/pkg/errors" 39 "github.com/urfave/cli" 40 "github.com/vbatts/go-mtree" 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 // WhiteoutMode indicates what style of whiteout was written to disk 94 // when this filesystem was extracted. 95 WhiteoutMode layer.WhiteoutMode `json:"whiteout_mode"` 96 } 97 98 // WriteTo writes a JSON-serialised version of Meta to the given io.Writer. 99 func (m Meta) WriteTo(w io.Writer) (int64, error) { 100 buf := new(bytes.Buffer) 101 err := json.NewEncoder(io.MultiWriter(buf, w)).Encode(m) 102 return int64(buf.Len()), err 103 } 104 105 // WriteBundleMeta writes an umoci.json file to the given bundle path. 106 func WriteBundleMeta(bundle string, meta Meta) error { 107 fh, err := os.Create(filepath.Join(bundle, MetaName)) 108 if err != nil { 109 return errors.Wrap(err, "create metadata") 110 } 111 defer fh.Close() 112 113 _, err = meta.WriteTo(fh) 114 return errors.Wrap(err, "write metadata") 115 } 116 117 // ReadBundleMeta reads and parses the umoci.json file from a given bundle path. 118 func ReadBundleMeta(bundle string) (Meta, error) { 119 var meta Meta 120 121 fh, err := os.Open(filepath.Join(bundle, MetaName)) 122 if err != nil { 123 return meta, errors.Wrap(err, "open metadata") 124 } 125 defer fh.Close() 126 127 err = json.NewDecoder(fh).Decode(&meta) 128 if meta.Version != MetaVersion { 129 if err == nil { 130 err = fmt.Errorf("unsupported umoci.json version: %s", meta.Version) 131 } 132 } 133 return meta, errors.Wrap(err, "decode metadata") 134 } 135 136 // ManifestStat has information about a given OCI manifest. 137 // TODO: Implement support for manifest lists, this should also be able to 138 // 139 // contain stat information for a list of manifests. 140 type ManifestStat struct { 141 // TODO: Flesh this out. Currently it's only really being used to get an 142 // equivalent of docker-history(1). We really need to add more 143 // information about it. 144 145 // History stores the history information for the manifest. 146 History []historyStat `json:"history"` 147 } 148 149 // Format formats a ManifestStat using the default formatting, and writes the 150 // result to the given writer. 151 // TODO: This should really be implemented in a way that allows for users to 152 // 153 // define their own custom templates for different blocks (meaning that 154 // this should use text/template rather than using tabwriters manually. 155 func (ms ManifestStat) Format(w io.Writer) error { 156 // Output history information. 157 tw := tabwriter.NewWriter(w, 4, 2, 1, ' ', 0) 158 fmt.Fprintf(tw, "LAYER\tCREATED\tCREATED BY\tSIZE\tCOMMENT\n") 159 for _, histEntry := range ms.History { 160 var ( 161 created = strings.Replace(histEntry.Created.Format(igen.ISO8601), "\t", " ", -1) 162 createdBy = strings.Replace(histEntry.CreatedBy, "\t", " ", -1) 163 comment = strings.Replace(histEntry.Comment, "\t", " ", -1) 164 layerID = "<none>" 165 size = "<none>" 166 ) 167 168 if !histEntry.EmptyLayer { 169 layerID = histEntry.Layer.Digest.String() 170 size = units.HumanSize(float64(histEntry.Layer.Size)) 171 } 172 173 // TODO: We need to truncate some of the fields. 174 fmt.Fprintf(tw, "%s\t%s\t%s\t%s\t%s\n", layerID, created, createdBy, size, comment) 175 } 176 return tw.Flush() 177 } 178 179 // historyStat contains information about a single entry in the history of a 180 // manifest. This is essentially equivalent to a single record from 181 // docker-history(1). 182 type historyStat struct { 183 // Layer is the descriptor referencing where the layer is stored. If it is 184 // nil, then this entry is an empty_layer (and thus doesn't have a backing 185 // diff layer). 186 Layer *ispec.Descriptor `json:"layer"` 187 188 // DiffID is an additional piece of information to Layer. It stores the 189 // DiffID of the given layer corresponding to the history entry. If DiffID 190 // is "", then this entry is an empty_layer. 191 DiffID string `json:"diff_id"` 192 193 // History is embedded in the stat information. 194 ispec.History 195 } 196 197 // Stat computes the ManifestStat for a given manifest blob. The provided 198 // descriptor must refer to an OCI Manifest. 199 func Stat(ctx context.Context, engine casext.Engine, manifestDescriptor ispec.Descriptor) (ManifestStat, error) { 200 var stat ManifestStat 201 202 if manifestDescriptor.MediaType != ispec.MediaTypeImageManifest { 203 return stat, errors.Errorf("stat: cannot stat a non-manifest descriptor: invalid media type '%s'", manifestDescriptor.MediaType) 204 } 205 206 // We have to get the actual manifest. 207 manifestBlob, err := engine.FromDescriptor(ctx, manifestDescriptor) 208 if err != nil { 209 return stat, err 210 } 211 manifest, ok := manifestBlob.Data.(ispec.Manifest) 212 if !ok { 213 // Should _never_ be reached. 214 return stat, errors.Errorf("[internal error] unknown manifest blob type: %s", manifestBlob.Descriptor.MediaType) 215 } 216 217 // Now get the config. 218 configBlob, err := engine.FromDescriptor(ctx, manifest.Config) 219 if err != nil { 220 return stat, errors.Wrap(err, "stat") 221 } 222 config, ok := configBlob.Data.(ispec.Image) 223 if !ok { 224 // Should _never_ be reached. 225 return stat, errors.Errorf("[internal error] unknown config blob type: %s", configBlob.Descriptor.MediaType) 226 } 227 228 // TODO: This should probably be moved into separate functions. 229 230 // Generate the history of the image. Because the config.History entries 231 // are in the same order as the manifest.Layer entries this is fairly 232 // simple. However, we only increment the layer index if a layer was 233 // actually generated by a history entry. 234 layerIdx := 0 235 for _, histEntry := range config.History { 236 info := historyStat{ 237 History: histEntry, 238 DiffID: "", 239 Layer: nil, 240 } 241 242 // Only fill the other information and increment layerIdx if it's a 243 // non-empty layer. 244 if !histEntry.EmptyLayer { 245 info.DiffID = config.RootFS.DiffIDs[layerIdx].String() 246 info.Layer = &manifest.Layers[layerIdx] 247 layerIdx++ 248 } 249 250 stat.History = append(stat.History, info) 251 } 252 253 return stat, nil 254 } 255 256 // GenerateBundleManifest creates and writes an mtree of the rootfs in the given 257 // bundle path, using the supplied fsEval method 258 func GenerateBundleManifest(mtreeName string, bundlePath string, fsEval mtree.FsEval) error { 259 mtreePath := filepath.Join(bundlePath, mtreeName+".mtree") 260 fullRootfsPath := filepath.Join(bundlePath, layer.RootfsName) 261 262 log.WithFields(log.Fields{ 263 "keywords": MtreeKeywords, 264 "mtree": mtreePath, 265 }).Debugf("umoci: generating mtree manifest") 266 267 log.Info("computing filesystem manifest ...") 268 dh, err := mtree.Walk(fullRootfsPath, nil, MtreeKeywords, fsEval) 269 if err != nil { 270 return errors.Wrap(err, "generate mtree spec") 271 } 272 log.Info("... done") 273 274 flags := os.O_CREATE | os.O_WRONLY | os.O_EXCL 275 fh, err := os.OpenFile(mtreePath, flags, 0644) 276 if err != nil { 277 return errors.Wrap(err, "open mtree") 278 } 279 defer fh.Close() 280 281 log.Debugf("umoci: saving mtree manifest") 282 283 if _, err := dh.WriteTo(fh); err != nil { 284 return errors.Wrap(err, "write mtree") 285 } 286 287 return nil 288 } 289 290 // ParseIdmapOptions sets up the mapping options for Meta, using 291 // the arguments specified on the command line 292 func ParseIdmapOptions(meta *Meta, ctx *cli.Context) error { 293 // We need to set mappings if we're in rootless mode. 294 meta.MapOptions.Rootless = ctx.Bool("rootless") 295 if meta.MapOptions.Rootless { 296 if !ctx.IsSet("uid-map") { 297 if err := ctx.Set("uid-map", fmt.Sprintf("0:%d:1", os.Geteuid())); err != nil { 298 // Should _never_ be reached. 299 return errors.Wrap(err, "[internal error] failure auto-setting rootless --uid-map") 300 } 301 } 302 if !ctx.IsSet("gid-map") { 303 if err := ctx.Set("gid-map", fmt.Sprintf("0:%d:1", os.Getegid())); err != nil { 304 // Should _never_ be reached. 305 return errors.Wrap(err, "[internal error] failure auto-setting rootless --gid-map") 306 } 307 } 308 } 309 310 for _, uidmap := range ctx.StringSlice("uid-map") { 311 idMap, err := idtools.ParseMapping(uidmap) 312 if err != nil { 313 return errors.Wrapf(err, "failure parsing --uid-map %s", uidmap) 314 } 315 meta.MapOptions.UIDMappings = append(meta.MapOptions.UIDMappings, idMap) 316 } 317 for _, gidmap := range ctx.StringSlice("gid-map") { 318 idMap, err := idtools.ParseMapping(gidmap) 319 if err != nil { 320 return errors.Wrapf(err, "failure parsing --gid-map %s", gidmap) 321 } 322 meta.MapOptions.GIDMappings = append(meta.MapOptions.GIDMappings, idMap) 323 } 324 325 log.WithFields(log.Fields{ 326 "map.uid": meta.MapOptions.UIDMappings, 327 "map.gid": meta.MapOptions.GIDMappings, 328 }).Debugf("parsed mappings") 329 330 return nil 331 }