github.com/dctrud/umoci@v0.4.3-0.20191016193643-05a1d37de015/cmd/umoci/repack.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 main 19 20 import ( 21 "fmt" 22 "os" 23 "path/filepath" 24 "strings" 25 "time" 26 27 "github.com/apex/log" 28 "github.com/openSUSE/umoci" 29 "github.com/openSUSE/umoci/mutate" 30 "github.com/openSUSE/umoci/oci/cas/dir" 31 "github.com/openSUSE/umoci/oci/casext" 32 igen "github.com/openSUSE/umoci/oci/config/generate" 33 "github.com/openSUSE/umoci/oci/layer" 34 "github.com/openSUSE/umoci/pkg/fseval" 35 "github.com/openSUSE/umoci/pkg/mtreefilter" 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 var repackCommand = uxHistory(cli.Command{ 44 Name: "repack", 45 Usage: "repacks an OCI runtime bundle into a reference", 46 ArgsUsage: `--image <image-path>[:<new-tag>] <bundle> 47 48 Where "<image-path>" is the path to the OCI image, "<new-tag>" is the name of 49 the tag that the new image will be saved as (if not specified, defaults to 50 "latest"), and "<bundle>" is the bundle from which to generate the required 51 layers. 52 53 The "<image-path>" MUST be the same image that was used to create "<bundle>" 54 (using umoci-unpack(1)). Otherwise umoci will not be able to modify the 55 original manifest to add the diff layer. 56 57 All uid-map and gid-map settings are automatically loaded from the bundle 58 metadata (which is generated by umoci-unpack(1)) so if you unpacked an image 59 using a particular mapping then the same mapping will be used to generate the 60 new layer. 61 62 It should be noted that this is not the same as oci-create-layer because it 63 uses go-mtree to create diff layers from runtime bundles unpacked with 64 umoci-unpack(1). In addition, it modifies the image so that all of the relevant 65 manifest and configuration information uses the new diff atop the old manifest.`, 66 67 // repack creates a new image, with a given tag. 68 Category: "image", 69 70 Flags: []cli.Flag{ 71 cli.StringSliceFlag{ 72 Name: "mask-path", 73 Usage: "set of path prefixes in which deltas will be ignored when generating new layers", 74 }, 75 cli.BoolFlag{ 76 Name: "no-mask-volumes", 77 Usage: "do not add the Config.Volumes of the image to the set of masked paths", 78 }, 79 cli.BoolFlag{ 80 Name: "refresh-bundle", 81 Usage: "update the bundle metadata to reflect the packed rootfs", 82 }, 83 }, 84 85 Action: repack, 86 87 Before: func(ctx *cli.Context) error { 88 if ctx.NArg() != 1 { 89 return errors.Errorf("invalid number of positional arguments: expected <bundle>") 90 } 91 if ctx.Args().First() == "" { 92 return errors.Errorf("bundle path cannot be empty") 93 } 94 ctx.App.Metadata["bundle"] = ctx.Args().First() 95 return nil 96 }, 97 }) 98 99 func repack(ctx *cli.Context) error { 100 imagePath := ctx.App.Metadata["--image-path"].(string) 101 tagName := ctx.App.Metadata["--image-tag"].(string) 102 bundlePath := ctx.App.Metadata["bundle"].(string) 103 104 // Read the metadata first. 105 meta, err := umoci.ReadBundleMeta(bundlePath) 106 if err != nil { 107 return errors.Wrap(err, "read umoci.json metadata") 108 } 109 110 log.WithFields(log.Fields{ 111 "version": meta.Version, 112 "from": meta.From, 113 "map_options": meta.MapOptions, 114 }).Debugf("umoci: loaded Meta metadata") 115 116 if meta.From.Descriptor().MediaType != ispec.MediaTypeImageManifest { 117 return errors.Wrap(fmt.Errorf("descriptor does not point to ispec.MediaTypeImageManifest: not implemented: %s", meta.From.Descriptor().MediaType), "invalid saved from descriptor") 118 } 119 120 // Get a reference to the CAS. 121 engine, err := dir.Open(imagePath) 122 if err != nil { 123 return errors.Wrap(err, "open CAS") 124 } 125 engineExt := casext.NewEngine(engine) 126 defer engine.Close() 127 128 // Create the mutator. 129 mutator, err := mutate.New(engine, meta.From) 130 if err != nil { 131 return errors.Wrap(err, "create mutator for base image") 132 } 133 134 mtreeName := strings.Replace(meta.From.Descriptor().Digest.String(), ":", "_", 1) 135 mtreePath := filepath.Join(bundlePath, mtreeName+".mtree") 136 fullRootfsPath := filepath.Join(bundlePath, layer.RootfsName) 137 138 log.WithFields(log.Fields{ 139 "image": imagePath, 140 "bundle": bundlePath, 141 "rootfs": layer.RootfsName, 142 "mtree": mtreePath, 143 }).Debugf("umoci: repacking OCI image") 144 145 mfh, err := os.Open(mtreePath) 146 if err != nil { 147 return errors.Wrap(err, "open mtree") 148 } 149 defer mfh.Close() 150 151 spec, err := mtree.ParseSpec(mfh) 152 if err != nil { 153 return errors.Wrap(err, "parse mtree") 154 } 155 156 log.WithFields(log.Fields{ 157 "keywords": umoci.MtreeKeywords, 158 }).Debugf("umoci: parsed mtree spec") 159 160 fsEval := fseval.DefaultFsEval 161 if meta.MapOptions.Rootless { 162 fsEval = fseval.RootlessFsEval 163 } 164 165 log.Info("computing filesystem diff ...") 166 diffs, err := mtree.Check(fullRootfsPath, spec, umoci.MtreeKeywords, fsEval) 167 if err != nil { 168 return errors.Wrap(err, "check mtree") 169 } 170 log.Info("... done") 171 172 log.WithFields(log.Fields{ 173 "ndiff": len(diffs), 174 }).Debugf("umoci: checked mtree spec") 175 176 // We need to mask config.Volumes. 177 config, err := mutator.Config(context.Background()) 178 if err != nil { 179 return errors.Wrap(err, "get config") 180 } 181 maskedPaths := ctx.StringSlice("mask-path") 182 if !ctx.Bool("no-mask-volumes") { 183 for v := range config.Volumes { 184 maskedPaths = append(maskedPaths, v) 185 } 186 } 187 diffs = mtreefilter.FilterDeltas(diffs, 188 mtreefilter.MaskFilter(maskedPaths), 189 mtreefilter.SimplifyFilter(diffs)) 190 191 reader, err := layer.GenerateLayer(fullRootfsPath, diffs, &meta.MapOptions) 192 if err != nil { 193 return errors.Wrap(err, "generate diff layer") 194 } 195 defer reader.Close() 196 197 imageMeta, err := mutator.Meta(context.Background()) 198 if err != nil { 199 return errors.Wrap(err, "get image metadata") 200 } 201 202 created := time.Now() 203 history := ispec.History{ 204 Author: imageMeta.Author, 205 Comment: "", 206 Created: &created, 207 CreatedBy: "umoci config", // XXX: Should we append argv to this? 208 EmptyLayer: false, 209 } 210 211 if val, ok := ctx.App.Metadata["--history.author"]; ok { 212 history.Author = val.(string) 213 } 214 if val, ok := ctx.App.Metadata["--history.comment"]; ok { 215 history.Comment = val.(string) 216 } 217 if val, ok := ctx.App.Metadata["--history.created"]; ok { 218 created, err := time.Parse(igen.ISO8601, val.(string)) 219 if err != nil { 220 return errors.Wrap(err, "parsing --history.created") 221 } 222 history.Created = &created 223 } 224 if val, ok := ctx.App.Metadata["--history.created_by"]; ok { 225 history.CreatedBy = val.(string) 226 } 227 228 // TODO: We should add a flag to allow for a new layer to be made 229 // non-distributable. 230 if err := mutator.Add(context.Background(), reader, history); err != nil { 231 return errors.Wrap(err, "add diff layer") 232 } 233 234 newDescriptorPath, err := mutator.Commit(context.Background()) 235 if err != nil { 236 return errors.Wrap(err, "commit mutated image") 237 } 238 239 log.Infof("new image manifest created: %s->%s", newDescriptorPath.Root().Digest, newDescriptorPath.Descriptor().Digest) 240 241 if err := engineExt.UpdateReference(context.Background(), tagName, newDescriptorPath.Root()); err != nil { 242 return errors.Wrap(err, "add new tag") 243 } 244 245 log.Infof("created new tag for image manifest: %s", tagName) 246 247 if ctx.Bool("refresh-bundle") { 248 newMtreeName := strings.Replace(newDescriptorPath.Descriptor().Digest.String(), ":", "_", 1) 249 if err := umoci.GenerateBundleManifest(newMtreeName, bundlePath, fsEval); err != nil { 250 return errors.Wrap(err, "write mtree metadata") 251 } 252 if err := os.Remove(mtreePath); err != nil { 253 return errors.Wrap(err, "remove old mtree metadata") 254 } 255 meta.From = newDescriptorPath 256 if err := umoci.WriteBundleMeta(bundlePath, meta); err != nil { 257 return errors.Wrap(err, "write umoci.json metadata") 258 } 259 } 260 261 return nil 262 }