github.com/dctrud/umoci@v0.4.3-0.20191016193643-05a1d37de015/cmd/umoci/insert.go (about)

     1  /*
     2   * umoci: Umoci Modifies Open Containers' Images
     3   * Copyright (C) 2016, 2017, 2018 SUSE LLC.
     4   * Copyright (C) 2018 Cisco
     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  	"github.com/openSUSE/umoci"
    27  	"github.com/openSUSE/umoci/mutate"
    28  	"github.com/openSUSE/umoci/oci/cas/dir"
    29  	"github.com/openSUSE/umoci/oci/casext"
    30  	igen "github.com/openSUSE/umoci/oci/config/generate"
    31  	"github.com/openSUSE/umoci/oci/layer"
    32  	ispec "github.com/opencontainers/image-spec/specs-go/v1"
    33  	"github.com/pkg/errors"
    34  	"github.com/urfave/cli"
    35  )
    36  
    37  var insertCommand = uxRemap(uxHistory(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  	tagName := ctx.App.Metadata["--image-tag"].(string)
   115  	sourcePath := ctx.App.Metadata["--source-path"].(string)
   116  	targetPath := ctx.App.Metadata["--target-path"].(string)
   117  
   118  	// Get a reference to the CAS.
   119  	engine, err := dir.Open(imagePath)
   120  	if err != nil {
   121  		return errors.Wrap(err, "open CAS")
   122  	}
   123  	engineExt := casext.NewEngine(engine)
   124  	defer engine.Close()
   125  
   126  	descriptorPaths, err := engineExt.ResolveReference(context.Background(), tagName)
   127  	if err != nil {
   128  		return errors.Wrap(err, "get descriptor")
   129  	}
   130  	if len(descriptorPaths) == 0 {
   131  		return errors.Errorf("tag not found: %s", tagName)
   132  	}
   133  	if len(descriptorPaths) != 1 {
   134  		// TODO: Handle this more nicely.
   135  		return errors.Errorf("tag is ambiguous: %s", tagName)
   136  	}
   137  
   138  	// Create the mutator.
   139  	mutator, err := mutate.New(engine, descriptorPaths[0])
   140  	if err != nil {
   141  		return errors.Wrap(err, "create mutator for base image")
   142  	}
   143  
   144  	var meta umoci.Meta
   145  	meta.Version = umoci.MetaVersion
   146  
   147  	// Parse and set up the mapping options.
   148  	err = umoci.ParseIdmapOptions(&meta, ctx)
   149  	if err != nil {
   150  		return err
   151  	}
   152  
   153  	reader := layer.GenerateInsertLayer(sourcePath, targetPath, ctx.IsSet("opaque"), &meta.MapOptions)
   154  	defer reader.Close()
   155  
   156  	created := time.Now()
   157  	history := ispec.History{
   158  		Comment:    "",
   159  		Created:    &created,
   160  		CreatedBy:  "umoci insert", // XXX: Should we append argv to this?
   161  		EmptyLayer: false,
   162  	}
   163  
   164  	if val, ok := ctx.App.Metadata["--history.author"]; ok {
   165  		history.Author = val.(string)
   166  	}
   167  	if val, ok := ctx.App.Metadata["--history.comment"]; ok {
   168  		history.Comment = val.(string)
   169  	}
   170  	if val, ok := ctx.App.Metadata["--history.created"]; ok {
   171  		created, err := time.Parse(igen.ISO8601, val.(string))
   172  		if err != nil {
   173  			return errors.Wrap(err, "parsing --history.created")
   174  		}
   175  		history.Created = &created
   176  	}
   177  	if val, ok := ctx.App.Metadata["--history.created_by"]; ok {
   178  		history.CreatedBy = val.(string)
   179  	}
   180  
   181  	// TODO: We should add a flag to allow for a new layer to be made
   182  	//       non-distributable.
   183  	if err := mutator.Add(context.Background(), reader, history); err != nil {
   184  		return errors.Wrap(err, "add diff layer")
   185  	}
   186  
   187  	newDescriptorPath, err := mutator.Commit(context.Background())
   188  	if err != nil {
   189  		return errors.Wrap(err, "commit mutated image")
   190  	}
   191  
   192  	log.Infof("new image manifest created: %s->%s", newDescriptorPath.Root().Digest, newDescriptorPath.Descriptor().Digest)
   193  
   194  	if err := engineExt.UpdateReference(context.Background(), tagName, newDescriptorPath.Root()); err != nil {
   195  		return errors.Wrap(err, "add new tag")
   196  	}
   197  	log.Infof("updated tag for image manifest: %s", tagName)
   198  	return nil
   199  }