github.com/opencontainers/umoci@v0.4.8-0.20240508124516-656e4836fb0d/cmd/umoci/config.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 "strings" 23 "time" 24 25 "github.com/apex/log" 26 ispec "github.com/opencontainers/image-spec/specs-go/v1" 27 "github.com/opencontainers/umoci/mutate" 28 "github.com/opencontainers/umoci/oci/cas/dir" 29 "github.com/opencontainers/umoci/oci/casext" 30 igen "github.com/opencontainers/umoci/oci/config/generate" 31 "github.com/pkg/errors" 32 "github.com/urfave/cli" 33 ) 34 35 // FIXME: We should also implement a raw mode that just does modifications of 36 // 37 // JSON blobs (allowing this all to be used outside of our build setup). 38 var configCommand = uxHistory(uxTag(cli.Command{ 39 Name: "config", 40 Usage: "modifies the image configuration of an OCI image", 41 ArgsUsage: `--image <image-path>[:<tag>] [--tag <new-tag>] 42 43 Where "<image-path>" is the path to the OCI image, and "<tag>" is the name of 44 the tagged image from which the config modifications will be based (if not 45 specified, it defaults to "latest"). "<new-tag>" is the new reference name to 46 save the new image as, if this is not specified then umoci will replace the old 47 image.`, 48 49 // config modifies a particular image manifest. 50 Category: "image", 51 52 // Verify the metadata. 53 Before: func(ctx *cli.Context) error { 54 if ctx.NArg() != 0 { 55 return errors.Errorf("invalid number of positional arguments: expected none") 56 } 57 if _, ok := ctx.App.Metadata["--image-path"]; !ok { 58 return errors.Errorf("missing mandatory argument: --image") 59 } 60 if _, ok := ctx.App.Metadata["--image-tag"]; !ok { 61 return errors.Errorf("missing mandatory argument: --image") 62 } 63 return nil 64 }, 65 66 // Do not re-order arguments. 67 // 68 // It turns out that urfave/cli incorrectly handles cases like 69 // [--config.cmd -c] during argument re-ordering for subcommands, causing 70 // us a fair number of issues when users are trying to pass a flag an 71 // argument that starts with a dash. Luckily 'umoci config' doesn't take 72 // positional arguments, so disabling argument re-ordering has no other 73 // real effect. 74 // 75 // See <https://github.com/urfave/cli/issues/1152> for more details. 76 SkipArgReorder: true, 77 78 Flags: []cli.Flag{ 79 cli.StringFlag{Name: "config.user"}, 80 cli.StringSliceFlag{Name: "config.exposedports"}, 81 cli.StringSliceFlag{Name: "config.env"}, 82 cli.StringSliceFlag{Name: "config.entrypoint"}, // FIXME: This interface is weird. 83 cli.StringSliceFlag{Name: "config.cmd"}, // FIXME: This interface is weird. 84 cli.StringSliceFlag{Name: "config.volume"}, 85 cli.StringSliceFlag{Name: "config.label"}, 86 cli.StringFlag{Name: "config.workingdir"}, 87 cli.StringFlag{Name: "config.stopsignal"}, 88 cli.StringFlag{Name: "created"}, // FIXME: Implement TimeFlag. 89 cli.StringFlag{Name: "author"}, 90 cli.StringFlag{Name: "architecture"}, 91 cli.StringFlag{Name: "os"}, 92 cli.StringSliceFlag{Name: "manifest.annotation"}, 93 cli.StringSliceFlag{Name: "clear"}, 94 }, 95 96 Action: config, 97 })) 98 99 func toImage(config ispec.ImageConfig, meta mutate.Meta) ispec.Image { 100 created := meta.Created 101 return ispec.Image{ 102 Config: config, 103 Created: &created, 104 Author: meta.Author, 105 Architecture: meta.Architecture, 106 OS: meta.OS, 107 } 108 } 109 110 func fromImage(image ispec.Image) (ispec.ImageConfig, mutate.Meta) { 111 var created time.Time 112 if image.Created != nil { 113 created = *image.Created 114 } 115 return image.Config, mutate.Meta{ 116 Created: created, 117 Author: image.Author, 118 Architecture: image.Architecture, 119 OS: image.OS, 120 } 121 } 122 123 // parseKV splits a given string (of the form name=value) into (name, 124 // value). An error is returned if there is no "=" in the line or if the 125 // name is empty. 126 func parseKV(input string) (string, string, error) { 127 parts := strings.SplitN(input, "=", 2) 128 if len(parts) != 2 { 129 return "", "", errors.Errorf("must contain '=': %s", input) 130 } 131 132 name, value := parts[0], parts[1] 133 if name == "" { 134 return "", "", errors.Errorf("must have non-empty name: %s", input) 135 } 136 return name, value, nil 137 } 138 139 func config(ctx *cli.Context) error { 140 imagePath := ctx.App.Metadata["--image-path"].(string) 141 fromName := ctx.App.Metadata["--image-tag"].(string) 142 143 // By default we clobber the old tag. 144 tagName := fromName 145 if val, ok := ctx.App.Metadata["--tag"]; ok { 146 tagName = val.(string) 147 } 148 149 // Get a reference to the CAS. 150 engine, err := dir.Open(imagePath) 151 if err != nil { 152 return errors.Wrap(err, "open CAS") 153 } 154 engineExt := casext.NewEngine(engine) 155 defer engine.Close() 156 157 fromDescriptorPaths, err := engineExt.ResolveReference(context.Background(), fromName) 158 if err != nil { 159 return errors.Wrap(err, "get descriptor") 160 } 161 if len(fromDescriptorPaths) == 0 { 162 return errors.Errorf("tag not found: %s", fromName) 163 } 164 if len(fromDescriptorPaths) != 1 { 165 // TODO: Handle this more nicely. 166 return errors.Errorf("tag is ambiguous: %s", fromName) 167 } 168 169 mutator, err := mutate.New(engine, fromDescriptorPaths[0]) 170 if err != nil { 171 return errors.Wrap(err, "create mutator for manifest") 172 } 173 174 config, err := mutator.Config(context.Background()) 175 if err != nil { 176 return errors.Wrap(err, "get base config") 177 } 178 179 imageMeta, err := mutator.Meta(context.Background()) 180 if err != nil { 181 return errors.Wrap(err, "get base metadata") 182 } 183 184 annotations, err := mutator.Annotations(context.Background()) 185 if err != nil { 186 return errors.Wrap(err, "get base annotations") 187 } 188 189 g, err := igen.NewFromImage(toImage(config.Config, imageMeta)) 190 if err != nil { 191 return errors.Wrap(err, "create new generator") 192 } 193 194 if ctx.IsSet("clear") { 195 for _, key := range ctx.StringSlice("clear") { 196 switch key { 197 case "config.labels": 198 g.ClearConfigLabels() 199 case "manifest.annotations": 200 annotations = nil 201 case "config.exposedports": 202 g.ClearConfigExposedPorts() 203 case "config.env": 204 g.ClearConfigEnv() 205 case "config.volume": 206 g.ClearConfigVolumes() 207 case "rootfs.diffids": 208 //g.ClearRootfsDiffIDs() 209 return errors.Errorf("--clear=rootfs.diffids is not safe") 210 case "config.cmd": 211 g.ClearConfigCmd() 212 case "config.entrypoint": 213 g.ClearConfigEntrypoint() 214 default: 215 return errors.Errorf("unknown key to --clear: %s", key) 216 } 217 } 218 } 219 220 if ctx.IsSet("created") { 221 // How do we handle other formats? 222 created, err := time.Parse(igen.ISO8601, ctx.String("created")) 223 if err != nil { 224 return errors.Wrap(err, "parse --created") 225 } 226 g.SetCreated(created) 227 } 228 if ctx.IsSet("author") { 229 g.SetAuthor(ctx.String("author")) 230 } 231 if ctx.IsSet("architecture") { 232 g.SetArchitecture(ctx.String("architecture")) 233 } 234 if ctx.IsSet("os") { 235 g.SetOS(ctx.String("os")) 236 } 237 if ctx.IsSet("config.user") { 238 g.SetConfigUser(ctx.String("config.user")) 239 } 240 if ctx.IsSet("config.stopsignal") { 241 g.SetConfigStopSignal(ctx.String("config.stopsignal")) 242 } 243 if ctx.IsSet("config.workingdir") { 244 g.SetConfigWorkingDir(ctx.String("config.workingdir")) 245 } 246 if ctx.IsSet("config.exposedports") { 247 for _, port := range ctx.StringSlice("config.exposedports") { 248 g.AddConfigExposedPort(port) 249 } 250 } 251 if ctx.IsSet("config.env") { 252 for _, env := range ctx.StringSlice("config.env") { 253 name, value, err := parseKV(env) 254 if err != nil { 255 return errors.Wrap(err, "config.env") 256 } 257 g.AddConfigEnv(name, value) 258 } 259 } 260 // FIXME: This interface is weird. 261 if ctx.IsSet("config.entrypoint") { 262 g.SetConfigEntrypoint(ctx.StringSlice("config.entrypoint")) 263 } 264 // FIXME: This interface is weird. 265 if ctx.IsSet("config.cmd") { 266 g.SetConfigCmd(ctx.StringSlice("config.cmd")) 267 } 268 if ctx.IsSet("config.volume") { 269 for _, volume := range ctx.StringSlice("config.volume") { 270 g.AddConfigVolume(volume) 271 } 272 } 273 if ctx.IsSet("config.label") { 274 for _, label := range ctx.StringSlice("config.label") { 275 name, value, err := parseKV(label) 276 if err != nil { 277 return errors.Wrap(err, "config.label") 278 } 279 g.AddConfigLabel(name, value) 280 } 281 } 282 if ctx.IsSet("manifest.annotation") { 283 if annotations == nil { 284 annotations = map[string]string{} 285 } 286 for _, label := range ctx.StringSlice("manifest.annotation") { 287 parts := strings.SplitN(label, "=", 2) 288 annotations[parts[0]] = parts[1] 289 } 290 } 291 292 var history *ispec.History 293 if !ctx.Bool("no-history") { 294 created := time.Now() 295 history = &ispec.History{ 296 Author: g.Author(), 297 Comment: "", 298 Created: &created, 299 CreatedBy: "umoci config", 300 EmptyLayer: true, 301 } 302 303 if ctx.IsSet("history.author") { 304 history.Author = ctx.String("history.author") 305 } 306 if ctx.IsSet("history.comment") { 307 history.Comment = ctx.String("history.comment") 308 } 309 if ctx.IsSet("history.created") { 310 created, err := time.Parse(igen.ISO8601, ctx.String("history.created")) 311 if err != nil { 312 return errors.Wrap(err, "parsing --history.created") 313 } 314 history.Created = &created 315 } 316 if ctx.IsSet("history.created_by") { 317 history.CreatedBy = ctx.String("history.created_by") 318 } 319 } 320 321 newConfig, newMeta := fromImage(g.Image()) 322 if err := mutator.Set(context.Background(), newConfig, newMeta, annotations, history); err != nil { 323 return errors.Wrap(err, "set modified configuration") 324 } 325 326 newDescriptorPath, err := mutator.Commit(context.Background()) 327 if err != nil { 328 return errors.Wrap(err, "commit mutated image") 329 } 330 331 log.Infof("new image manifest created: %s->%s", newDescriptorPath.Root().Digest, newDescriptorPath.Descriptor().Digest) 332 333 if err := engineExt.UpdateReference(context.Background(), tagName, newDescriptorPath.Root()); err != nil { 334 return errors.Wrap(err, "add new tag") 335 } 336 337 log.Infof("created new tag for image manifest: %s", tagName) 338 return nil 339 }