github.com/opencontainers/umoci@v0.4.8-0.20240508124516-656e4836fb0d/cmd/umoci/repack.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 main 19 20 import ( 21 "context" 22 "fmt" 23 "time" 24 25 "github.com/apex/log" 26 ispec "github.com/opencontainers/image-spec/specs-go/v1" 27 "github.com/opencontainers/umoci" 28 "github.com/opencontainers/umoci/mutate" 29 "github.com/opencontainers/umoci/oci/cas/dir" 30 "github.com/opencontainers/umoci/oci/casext" 31 igen "github.com/opencontainers/umoci/oci/config/generate" 32 "github.com/opencontainers/umoci/pkg/mtreefilter" 33 "github.com/pkg/errors" 34 "github.com/urfave/cli" 35 ) 36 37 var repackCommand = uxHistory(cli.Command{ 38 Name: "repack", 39 Usage: "repacks an OCI runtime bundle into a reference", 40 ArgsUsage: `--image <image-path>[:<new-tag>] <bundle> 41 42 Where "<image-path>" is the path to the OCI image, "<new-tag>" is the name of 43 the tag that the new image will be saved as (if not specified, defaults to 44 "latest"), and "<bundle>" is the bundle from which to generate the required 45 layers. 46 47 The "<image-path>" MUST be the same image that was used to create "<bundle>" 48 (using umoci-unpack(1)). Otherwise umoci will not be able to modify the 49 original manifest to add the diff layer. 50 51 All uid-map and gid-map settings are automatically loaded from the bundle 52 metadata (which is generated by umoci-unpack(1)) so if you unpacked an image 53 using a particular mapping then the same mapping will be used to generate the 54 new layer. 55 56 It should be noted that this is not the same as oci-create-layer because it 57 uses go-mtree to create diff layers from runtime bundles unpacked with 58 umoci-unpack(1). In addition, it modifies the image so that all of the relevant 59 manifest and configuration information uses the new diff atop the old manifest.`, 60 61 // repack creates a new image, with a given tag. 62 Category: "image", 63 64 Flags: []cli.Flag{ 65 cli.StringSliceFlag{ 66 Name: "mask-path", 67 Usage: "set of path prefixes in which deltas will be ignored when generating new layers", 68 }, 69 cli.BoolFlag{ 70 Name: "no-mask-volumes", 71 Usage: "do not add the Config.Volumes of the image to the set of masked paths", 72 }, 73 cli.BoolFlag{ 74 Name: "refresh-bundle", 75 Usage: "update the bundle metadata to reflect the packed rootfs", 76 }, 77 }, 78 79 Action: repack, 80 81 Before: func(ctx *cli.Context) error { 82 if ctx.NArg() != 1 { 83 return errors.Errorf("invalid number of positional arguments: expected <bundle>") 84 } 85 if ctx.Args().First() == "" { 86 return errors.Errorf("bundle path cannot be empty") 87 } 88 ctx.App.Metadata["bundle"] = ctx.Args().First() 89 return nil 90 }, 91 }) 92 93 func repack(ctx *cli.Context) error { 94 imagePath := ctx.App.Metadata["--image-path"].(string) 95 tagName := ctx.App.Metadata["--image-tag"].(string) 96 bundlePath := ctx.App.Metadata["bundle"].(string) 97 98 // Read the metadata first. 99 meta, err := umoci.ReadBundleMeta(bundlePath) 100 if err != nil { 101 return errors.Wrap(err, "read umoci.json metadata") 102 } 103 104 log.WithFields(log.Fields{ 105 "version": meta.Version, 106 "from": meta.From, 107 "map_options": meta.MapOptions, 108 }).Debugf("umoci: loaded Meta metadata") 109 110 if meta.From.Descriptor().MediaType != ispec.MediaTypeImageManifest { 111 return errors.Wrap(fmt.Errorf("descriptor does not point to ispec.MediaTypeImageManifest: not implemented: %s", meta.From.Descriptor().MediaType), "invalid saved from descriptor") 112 } 113 114 // Get a reference to the CAS. 115 engine, err := dir.Open(imagePath) 116 if err != nil { 117 return errors.Wrap(err, "open CAS") 118 } 119 engineExt := casext.NewEngine(engine) 120 defer engine.Close() 121 122 // Create the mutator. 123 mutator, err := mutate.New(engineExt, meta.From) 124 if err != nil { 125 return errors.Wrap(err, "create mutator for base image") 126 } 127 128 // We need to mask config.Volumes. 129 config, err := mutator.Config(context.Background()) 130 if err != nil { 131 return errors.Wrap(err, "get config") 132 } 133 134 maskedPaths := ctx.StringSlice("mask-path") 135 if !ctx.Bool("no-mask-volumes") { 136 for v := range config.Config.Volumes { 137 maskedPaths = append(maskedPaths, v) 138 } 139 } 140 141 imageMeta, err := mutator.Meta(context.Background()) 142 if err != nil { 143 return errors.Wrap(err, "get image metadata") 144 } 145 146 var history *ispec.History 147 if !ctx.Bool("no-history") { 148 created := time.Now() 149 history = &ispec.History{ 150 Author: imageMeta.Author, 151 Comment: "", 152 Created: &created, 153 CreatedBy: "umoci repack", // XXX: Should we append argv to this? 154 EmptyLayer: false, 155 } 156 157 if ctx.IsSet("history.author") { 158 history.Author = ctx.String("history.author") 159 } 160 if ctx.IsSet("history.comment") { 161 history.Comment = ctx.String("history.comment") 162 } 163 if ctx.IsSet("history.created") { 164 created, err := time.Parse(igen.ISO8601, ctx.String("history.created")) 165 if err != nil { 166 return errors.Wrap(err, "parsing --history.created") 167 } 168 history.Created = &created 169 } 170 if ctx.IsSet("history.created_by") { 171 history.CreatedBy = ctx.String("history.created_by") 172 } 173 } 174 175 filters := []mtreefilter.FilterFunc{ 176 mtreefilter.MaskFilter(maskedPaths), 177 } 178 179 return umoci.Repack(engineExt, tagName, bundlePath, meta, history, filters, ctx.Bool("refresh-bundle"), mutator) 180 }