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