github.com/dctrud/umoci@v0.4.3-0.20191016193643-05a1d37de015/cmd/umoci/repack.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  	"fmt"
    22  	"os"
    23  	"path/filepath"
    24  	"strings"
    25  	"time"
    26  
    27  	"github.com/apex/log"
    28  	"github.com/openSUSE/umoci"
    29  	"github.com/openSUSE/umoci/mutate"
    30  	"github.com/openSUSE/umoci/oci/cas/dir"
    31  	"github.com/openSUSE/umoci/oci/casext"
    32  	igen "github.com/openSUSE/umoci/oci/config/generate"
    33  	"github.com/openSUSE/umoci/oci/layer"
    34  	"github.com/openSUSE/umoci/pkg/fseval"
    35  	"github.com/openSUSE/umoci/pkg/mtreefilter"
    36  	ispec "github.com/opencontainers/image-spec/specs-go/v1"
    37  	"github.com/pkg/errors"
    38  	"github.com/urfave/cli"
    39  	"github.com/vbatts/go-mtree"
    40  	"golang.org/x/net/context"
    41  )
    42  
    43  var repackCommand = uxHistory(cli.Command{
    44  	Name:  "repack",
    45  	Usage: "repacks an OCI runtime bundle into a reference",
    46  	ArgsUsage: `--image <image-path>[:<new-tag>] <bundle>
    47  
    48  Where "<image-path>" is the path to the OCI image, "<new-tag>" is the name of
    49  the tag that the new image will be saved as (if not specified, defaults to
    50  "latest"), and "<bundle>" is the bundle from which to generate the required
    51  layers.
    52  
    53  The "<image-path>" MUST be the same image that was used to create "<bundle>"
    54  (using umoci-unpack(1)). Otherwise umoci will not be able to modify the
    55  original manifest to add the diff layer.
    56  
    57  All uid-map and gid-map settings are automatically loaded from the bundle
    58  metadata (which is generated by umoci-unpack(1)) so if you unpacked an image
    59  using a particular mapping then the same mapping will be used to generate the
    60  new layer.
    61  
    62  It should be noted that this is not the same as oci-create-layer because it
    63  uses go-mtree to create diff layers from runtime bundles unpacked with
    64  umoci-unpack(1). In addition, it modifies the image so that all of the relevant
    65  manifest and configuration information uses the new diff atop the old manifest.`,
    66  
    67  	// repack creates a new image, with a given tag.
    68  	Category: "image",
    69  
    70  	Flags: []cli.Flag{
    71  		cli.StringSliceFlag{
    72  			Name:  "mask-path",
    73  			Usage: "set of path prefixes in which deltas will be ignored when generating new layers",
    74  		},
    75  		cli.BoolFlag{
    76  			Name:  "no-mask-volumes",
    77  			Usage: "do not add the Config.Volumes of the image to the set of masked paths",
    78  		},
    79  		cli.BoolFlag{
    80  			Name:  "refresh-bundle",
    81  			Usage: "update the bundle metadata to reflect the packed rootfs",
    82  		},
    83  	},
    84  
    85  	Action: repack,
    86  
    87  	Before: func(ctx *cli.Context) error {
    88  		if ctx.NArg() != 1 {
    89  			return errors.Errorf("invalid number of positional arguments: expected <bundle>")
    90  		}
    91  		if ctx.Args().First() == "" {
    92  			return errors.Errorf("bundle path cannot be empty")
    93  		}
    94  		ctx.App.Metadata["bundle"] = ctx.Args().First()
    95  		return nil
    96  	},
    97  })
    98  
    99  func repack(ctx *cli.Context) error {
   100  	imagePath := ctx.App.Metadata["--image-path"].(string)
   101  	tagName := ctx.App.Metadata["--image-tag"].(string)
   102  	bundlePath := ctx.App.Metadata["bundle"].(string)
   103  
   104  	// Read the metadata first.
   105  	meta, err := umoci.ReadBundleMeta(bundlePath)
   106  	if err != nil {
   107  		return errors.Wrap(err, "read umoci.json metadata")
   108  	}
   109  
   110  	log.WithFields(log.Fields{
   111  		"version":     meta.Version,
   112  		"from":        meta.From,
   113  		"map_options": meta.MapOptions,
   114  	}).Debugf("umoci: loaded Meta metadata")
   115  
   116  	if meta.From.Descriptor().MediaType != ispec.MediaTypeImageManifest {
   117  		return errors.Wrap(fmt.Errorf("descriptor does not point to ispec.MediaTypeImageManifest: not implemented: %s", meta.From.Descriptor().MediaType), "invalid saved from descriptor")
   118  	}
   119  
   120  	// Get a reference to the CAS.
   121  	engine, err := dir.Open(imagePath)
   122  	if err != nil {
   123  		return errors.Wrap(err, "open CAS")
   124  	}
   125  	engineExt := casext.NewEngine(engine)
   126  	defer engine.Close()
   127  
   128  	// Create the mutator.
   129  	mutator, err := mutate.New(engine, meta.From)
   130  	if err != nil {
   131  		return errors.Wrap(err, "create mutator for base image")
   132  	}
   133  
   134  	mtreeName := strings.Replace(meta.From.Descriptor().Digest.String(), ":", "_", 1)
   135  	mtreePath := filepath.Join(bundlePath, mtreeName+".mtree")
   136  	fullRootfsPath := filepath.Join(bundlePath, layer.RootfsName)
   137  
   138  	log.WithFields(log.Fields{
   139  		"image":  imagePath,
   140  		"bundle": bundlePath,
   141  		"rootfs": layer.RootfsName,
   142  		"mtree":  mtreePath,
   143  	}).Debugf("umoci: repacking OCI image")
   144  
   145  	mfh, err := os.Open(mtreePath)
   146  	if err != nil {
   147  		return errors.Wrap(err, "open mtree")
   148  	}
   149  	defer mfh.Close()
   150  
   151  	spec, err := mtree.ParseSpec(mfh)
   152  	if err != nil {
   153  		return errors.Wrap(err, "parse mtree")
   154  	}
   155  
   156  	log.WithFields(log.Fields{
   157  		"keywords": umoci.MtreeKeywords,
   158  	}).Debugf("umoci: parsed mtree spec")
   159  
   160  	fsEval := fseval.DefaultFsEval
   161  	if meta.MapOptions.Rootless {
   162  		fsEval = fseval.RootlessFsEval
   163  	}
   164  
   165  	log.Info("computing filesystem diff ...")
   166  	diffs, err := mtree.Check(fullRootfsPath, spec, umoci.MtreeKeywords, fsEval)
   167  	if err != nil {
   168  		return errors.Wrap(err, "check mtree")
   169  	}
   170  	log.Info("... done")
   171  
   172  	log.WithFields(log.Fields{
   173  		"ndiff": len(diffs),
   174  	}).Debugf("umoci: checked mtree spec")
   175  
   176  	// We need to mask config.Volumes.
   177  	config, err := mutator.Config(context.Background())
   178  	if err != nil {
   179  		return errors.Wrap(err, "get config")
   180  	}
   181  	maskedPaths := ctx.StringSlice("mask-path")
   182  	if !ctx.Bool("no-mask-volumes") {
   183  		for v := range config.Volumes {
   184  			maskedPaths = append(maskedPaths, v)
   185  		}
   186  	}
   187  	diffs = mtreefilter.FilterDeltas(diffs,
   188  		mtreefilter.MaskFilter(maskedPaths),
   189  		mtreefilter.SimplifyFilter(diffs))
   190  
   191  	reader, err := layer.GenerateLayer(fullRootfsPath, diffs, &meta.MapOptions)
   192  	if err != nil {
   193  		return errors.Wrap(err, "generate diff layer")
   194  	}
   195  	defer reader.Close()
   196  
   197  	imageMeta, err := mutator.Meta(context.Background())
   198  	if err != nil {
   199  		return errors.Wrap(err, "get image metadata")
   200  	}
   201  
   202  	created := time.Now()
   203  	history := ispec.History{
   204  		Author:     imageMeta.Author,
   205  		Comment:    "",
   206  		Created:    &created,
   207  		CreatedBy:  "umoci config", // XXX: Should we append argv to this?
   208  		EmptyLayer: false,
   209  	}
   210  
   211  	if val, ok := ctx.App.Metadata["--history.author"]; ok {
   212  		history.Author = val.(string)
   213  	}
   214  	if val, ok := ctx.App.Metadata["--history.comment"]; ok {
   215  		history.Comment = val.(string)
   216  	}
   217  	if val, ok := ctx.App.Metadata["--history.created"]; ok {
   218  		created, err := time.Parse(igen.ISO8601, val.(string))
   219  		if err != nil {
   220  			return errors.Wrap(err, "parsing --history.created")
   221  		}
   222  		history.Created = &created
   223  	}
   224  	if val, ok := ctx.App.Metadata["--history.created_by"]; ok {
   225  		history.CreatedBy = val.(string)
   226  	}
   227  
   228  	// TODO: We should add a flag to allow for a new layer to be made
   229  	//       non-distributable.
   230  	if err := mutator.Add(context.Background(), reader, history); err != nil {
   231  		return errors.Wrap(err, "add diff layer")
   232  	}
   233  
   234  	newDescriptorPath, err := mutator.Commit(context.Background())
   235  	if err != nil {
   236  		return errors.Wrap(err, "commit mutated image")
   237  	}
   238  
   239  	log.Infof("new image manifest created: %s->%s", newDescriptorPath.Root().Digest, newDescriptorPath.Descriptor().Digest)
   240  
   241  	if err := engineExt.UpdateReference(context.Background(), tagName, newDescriptorPath.Root()); err != nil {
   242  		return errors.Wrap(err, "add new tag")
   243  	}
   244  
   245  	log.Infof("created new tag for image manifest: %s", tagName)
   246  
   247  	if ctx.Bool("refresh-bundle") {
   248  		newMtreeName := strings.Replace(newDescriptorPath.Descriptor().Digest.String(), ":", "_", 1)
   249  		if err := umoci.GenerateBundleManifest(newMtreeName, bundlePath, fsEval); err != nil {
   250  			return errors.Wrap(err, "write mtree metadata")
   251  		}
   252  		if err := os.Remove(mtreePath); err != nil {
   253  			return errors.Wrap(err, "remove old mtree metadata")
   254  		}
   255  		meta.From = newDescriptorPath
   256  		if err := umoci.WriteBundleMeta(bundlePath, meta); err != nil {
   257  			return errors.Wrap(err, "write umoci.json metadata")
   258  		}
   259  	}
   260  
   261  	return nil
   262  }