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  }