github.com/opencontainers/umoci@v0.4.8-0.20240508124516-656e4836fb0d/cmd/umoci/insert.go (about) 1 /* 2 * umoci: Umoci Modifies Open Containers' Images 3 * Copyright (C) 2016-2020 SUSE LLC 4 * Copyright (C) 2018 Cisco Systems 5 * 6 * Licensed under the Apache License, Version 2.0 (the "License"); 7 * you may not use this file except in compliance with the License. 8 * You may obtain a copy of the License at 9 * 10 * http://www.apache.org/licenses/LICENSE-2.0 11 * 12 * Unless required by applicable law or agreed to in writing, software 13 * distributed under the License is distributed on an "AS IS" BASIS, 14 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 * See the License for the specific language governing permissions and 16 * limitations under the License. 17 */ 18 19 package main 20 21 import ( 22 "context" 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/oci/layer" 33 "github.com/pkg/errors" 34 "github.com/urfave/cli" 35 ) 36 37 var insertCommand = uxRemap(uxHistory(uxTag(cli.Command{ 38 Name: "insert", 39 Usage: "insert content into an OCI image", 40 ArgsUsage: `--image <image-path>[:<tag>] [--opaque] <source> <target> 41 --image <image-path>[:<tag>] [--whiteout] <target> 42 43 Where "<image-path>" is the path to the OCI image, and "<tag>" is the name of 44 the tag that the content wil be inserted into (if not specified, defaults to 45 "latest"). 46 47 The path at "<source>" is added to the image with the given "<target>" name. 48 If "--whiteout" is specified, rather than inserting content into the image, a 49 removal entry for "<target>" is inserted instead. 50 51 If "--opaque" is specified then any paths below "<target>" (assuming it is a 52 directory) from previous layers will no longer be present. Only the contents 53 inserted by this command will be visible. This can be used to replace an entire 54 directory, while the default behaviour merges the old contents with the new. 55 56 Note that this command works by creating a new layer, so this should not be 57 used to remove (or replace) secrets from an already-built image. See 58 umoci-config(1) and --config.volume for how to achieve this correctly. 59 60 Some examples: 61 umoci insert --image oci:foo mybinary /usr/bin/mybinary 62 umoci insert --image oci:foo myconfigdir /etc/myconfigdir 63 umoci insert --image oci:foo --opaque myoptdir /opt 64 umoci insert --image oci:foo --whiteout /some/old/dir 65 `, 66 67 Category: "image", 68 69 Action: insert, 70 71 Flags: []cli.Flag{ 72 cli.BoolFlag{ 73 Name: "whiteout", 74 Usage: "insert a 'removal entry' for the given path", 75 }, 76 cli.BoolFlag{ 77 Name: "opaque", 78 Usage: "mask any previous entries in the target directory", 79 }, 80 }, 81 82 Before: func(ctx *cli.Context) error { 83 // This command is quite weird because we need to support two different 84 // positional-argument numbers. Awesome. 85 numArgs := 2 86 if ctx.IsSet("whiteout") { 87 numArgs = 1 88 } 89 if ctx.NArg() != numArgs { 90 return errors.Errorf("invalid number of positional arguments: expected %d", numArgs) 91 } 92 for idx, args := range ctx.Args() { 93 if args == "" { 94 return errors.Errorf("invalid positional argument %d: arguments cannot be empty", idx) 95 } 96 } 97 98 // Figure out the arguments. 99 var sourcePath, targetPath string 100 targetPath = ctx.Args()[0] 101 if !ctx.IsSet("whiteout") { 102 sourcePath = targetPath 103 targetPath = ctx.Args()[1] 104 } 105 106 ctx.App.Metadata["--source-path"] = sourcePath 107 ctx.App.Metadata["--target-path"] = targetPath 108 return nil 109 }, 110 }))) 111 112 func insert(ctx *cli.Context) error { 113 imagePath := ctx.App.Metadata["--image-path"].(string) 114 fromName := ctx.App.Metadata["--image-tag"].(string) 115 sourcePath := ctx.App.Metadata["--source-path"].(string) 116 targetPath := ctx.App.Metadata["--target-path"].(string) 117 118 // By default we clobber the old tag. 119 tagName := fromName 120 if val, ok := ctx.App.Metadata["--tag"]; ok { 121 tagName = val.(string) 122 } 123 124 // Get a reference to the CAS. 125 engine, err := dir.Open(imagePath) 126 if err != nil { 127 return errors.Wrap(err, "open CAS") 128 } 129 engineExt := casext.NewEngine(engine) 130 defer engine.Close() 131 132 descriptorPaths, err := engineExt.ResolveReference(context.Background(), fromName) 133 if err != nil { 134 return errors.Wrap(err, "get descriptor") 135 } 136 if len(descriptorPaths) == 0 { 137 return errors.Errorf("tag not found: %s", fromName) 138 } 139 if len(descriptorPaths) != 1 { 140 // TODO: Handle this more nicely. 141 return errors.Errorf("tag is ambiguous: %s", fromName) 142 } 143 144 // Create the mutator. 145 mutator, err := mutate.New(engine, descriptorPaths[0]) 146 if err != nil { 147 return errors.Wrap(err, "create mutator for base image") 148 } 149 150 var meta umoci.Meta 151 meta.Version = umoci.MetaVersion 152 153 // Parse and set up the mapping options. 154 err = umoci.ParseIdmapOptions(&meta, ctx) 155 if err != nil { 156 return err 157 } 158 159 packOptions := layer.RepackOptions{MapOptions: meta.MapOptions} 160 reader := layer.GenerateInsertLayer(sourcePath, targetPath, ctx.IsSet("opaque"), &packOptions) 161 defer reader.Close() 162 163 var history *ispec.History 164 if !ctx.Bool("no-history") { 165 created := time.Now() 166 history = &ispec.History{ 167 Comment: "", 168 Created: &created, 169 CreatedBy: "umoci insert", // XXX: Should we append argv to this? 170 EmptyLayer: false, 171 } 172 173 if ctx.IsSet("history.author") { 174 history.Author = ctx.String("history.author") 175 } 176 if ctx.IsSet("history.comment") { 177 history.Comment = ctx.String("history.comment") 178 } 179 if ctx.IsSet("history.created") { 180 created, err := time.Parse(igen.ISO8601, ctx.String("history.created")) 181 if err != nil { 182 return errors.Wrap(err, "parsing --history.created") 183 } 184 history.Created = &created 185 } 186 if ctx.IsSet("history.created_by") { 187 history.CreatedBy = ctx.String("history.created_by") 188 } 189 } 190 191 // TODO: We should add a flag to allow for a new layer to be made 192 // non-distributable. 193 if _, err := mutator.Add(context.Background(), ispec.MediaTypeImageLayer, reader, history, mutate.GzipCompressor, nil); err != nil { 194 return errors.Wrap(err, "add diff layer") 195 } 196 197 newDescriptorPath, err := mutator.Commit(context.Background()) 198 if err != nil { 199 return errors.Wrap(err, "commit mutated image") 200 } 201 202 log.Infof("new image manifest created: %s->%s", newDescriptorPath.Root().Digest, newDescriptorPath.Descriptor().Digest) 203 204 if err := engineExt.UpdateReference(context.Background(), tagName, newDescriptorPath.Root()); err != nil { 205 return errors.Wrap(err, "add new tag") 206 } 207 log.Infof("updated tag for image manifest: %s", tagName) 208 return nil 209 }