github.hscsec.cn/opencontainers/umoci@v0.4.2/utils.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 umoci
    19  
    20  import (
    21  	"bytes"
    22  	"encoding/json"
    23  	"fmt"
    24  	"io"
    25  	"os"
    26  	"path/filepath"
    27  	"strings"
    28  	"text/tabwriter"
    29  
    30  	"github.com/apex/log"
    31  	"github.com/docker/go-units"
    32  	"github.com/openSUSE/umoci/oci/casext"
    33  	igen "github.com/openSUSE/umoci/oci/config/generate"
    34  	"github.com/openSUSE/umoci/oci/layer"
    35  	"github.com/openSUSE/umoci/pkg/idtools"
    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  // FIXME: This should be moved to a library. Too much of this code is in the
    44  //        cmd/... code, but should really be refactored to the point where it
    45  //        can be useful to other people. This is _particularly_ true for the
    46  //        code which repacks images (the changes to the config, manifest and
    47  //        CAS should be made into a library).
    48  
    49  // MtreeKeywords is the set of keywords used by umoci for verification and diff
    50  // generation of a bundle. This is based on mtree.DefaultKeywords, but is
    51  // hardcoded here to ensure that vendor changes don't mess things up.
    52  var MtreeKeywords = []mtree.Keyword{
    53  	"size",
    54  	"type",
    55  	"uid",
    56  	"gid",
    57  	"mode",
    58  	"link",
    59  	"nlink",
    60  	"tar_time",
    61  	"sha256digest",
    62  	"xattr",
    63  }
    64  
    65  // MetaName is the name of umoci's metadata file that is stored in all
    66  // bundles extracted by umoci.
    67  const MetaName = "umoci.json"
    68  
    69  // MetaVersion is the version of Meta supported by this code. The
    70  // value is only bumped for updates which are not backwards compatible.
    71  const MetaVersion = "2"
    72  
    73  // Meta represents metadata about how umoci unpacked an image to a bundle
    74  // and other similar information. It is used to keep track of information that
    75  // is required when repacking an image and other similar bundle information.
    76  type Meta struct {
    77  	// Version is the version of umoci used to unpack the bundle. This is used
    78  	// to future-proof the umoci.json information.
    79  	Version string `json:"umoci_version"`
    80  
    81  	// From is a copy of the descriptor pointing to the image manifest that was
    82  	// used to unpack the bundle. Essentially it's a resolved form of the
    83  	// --image argument to umoci-unpack(1).
    84  	From casext.DescriptorPath `json:"from_descriptor_path"`
    85  
    86  	// MapOptions is the parsed version of --uid-map, --gid-map and --rootless
    87  	// arguments to umoci-unpack(1). While all of these options technically do
    88  	// not need to be the same for corresponding umoci-unpack(1) and
    89  	// umoci-repack(1) calls, changing them is not recommended and so the
    90  	// default should be that they are the same.
    91  	MapOptions layer.MapOptions `json:"map_options"`
    92  }
    93  
    94  // WriteTo writes a JSON-serialised version of Meta to the given io.Writer.
    95  func (m Meta) WriteTo(w io.Writer) (int64, error) {
    96  	buf := new(bytes.Buffer)
    97  	err := json.NewEncoder(io.MultiWriter(buf, w)).Encode(m)
    98  	return int64(buf.Len()), err
    99  }
   100  
   101  // WriteBundleMeta writes an umoci.json file to the given bundle path.
   102  func WriteBundleMeta(bundle string, meta Meta) error {
   103  	fh, err := os.Create(filepath.Join(bundle, MetaName))
   104  	if err != nil {
   105  		return errors.Wrap(err, "create metadata")
   106  	}
   107  	defer fh.Close()
   108  
   109  	_, err = meta.WriteTo(fh)
   110  	return errors.Wrap(err, "write metadata")
   111  }
   112  
   113  // ReadBundleMeta reads and parses the umoci.json file from a given bundle path.
   114  func ReadBundleMeta(bundle string) (Meta, error) {
   115  	var meta Meta
   116  
   117  	fh, err := os.Open(filepath.Join(bundle, MetaName))
   118  	if err != nil {
   119  		return meta, errors.Wrap(err, "open metadata")
   120  	}
   121  	defer fh.Close()
   122  
   123  	err = json.NewDecoder(fh).Decode(&meta)
   124  	if meta.Version != MetaVersion {
   125  		if err == nil {
   126  			err = fmt.Errorf("unsupported umoci.json version: %s", meta.Version)
   127  		}
   128  	}
   129  	return meta, errors.Wrap(err, "decode metadata")
   130  }
   131  
   132  // ManifestStat has information about a given OCI manifest.
   133  // TODO: Implement support for manifest lists, this should also be able to
   134  //       contain stat information for a list of manifests.
   135  type ManifestStat struct {
   136  	// TODO: Flesh this out. Currently it's only really being used to get an
   137  	//       equivalent of docker-history(1). We really need to add more
   138  	//       information about it.
   139  
   140  	// History stores the history information for the manifest.
   141  	History []historyStat `json:"history"`
   142  }
   143  
   144  // Format formats a ManifestStat using the default formatting, and writes the
   145  // result to the given writer.
   146  // TODO: This should really be implemented in a way that allows for users to
   147  //       define their own custom templates for different blocks (meaning that
   148  //       this should use text/template rather than using tabwriters manually.
   149  func (ms ManifestStat) Format(w io.Writer) error {
   150  	// Output history information.
   151  	tw := tabwriter.NewWriter(w, 4, 2, 1, ' ', 0)
   152  	fmt.Fprintf(tw, "LAYER\tCREATED\tCREATED BY\tSIZE\tCOMMENT\n")
   153  	for _, histEntry := range ms.History {
   154  		var (
   155  			created   = strings.Replace(histEntry.Created.Format(igen.ISO8601), "\t", " ", -1)
   156  			createdBy = strings.Replace(histEntry.CreatedBy, "\t", " ", -1)
   157  			comment   = strings.Replace(histEntry.Comment, "\t", " ", -1)
   158  			layerID   = "<none>"
   159  			size      = "<none>"
   160  		)
   161  
   162  		if !histEntry.EmptyLayer {
   163  			layerID = histEntry.Layer.Digest.String()
   164  			size = units.HumanSize(float64(histEntry.Layer.Size))
   165  		}
   166  
   167  		// TODO: We need to truncate some of the fields.
   168  
   169  		fmt.Fprintf(tw, "%s\t%s\t%s\t%s\t%s\n", layerID, created, createdBy, size, comment)
   170  	}
   171  	tw.Flush()
   172  	return nil
   173  }
   174  
   175  // historyStat contains information about a single entry in the history of a
   176  // manifest. This is essentially equivalent to a single record from
   177  // docker-history(1).
   178  type historyStat struct {
   179  	// Layer is the descriptor referencing where the layer is stored. If it is
   180  	// nil, then this entry is an empty_layer (and thus doesn't have a backing
   181  	// diff layer).
   182  	Layer *ispec.Descriptor `json:"layer"`
   183  
   184  	// DiffID is an additional piece of information to Layer. It stores the
   185  	// DiffID of the given layer corresponding to the history entry. If DiffID
   186  	// is "", then this entry is an empty_layer.
   187  	DiffID string `json:"diff_id"`
   188  
   189  	// History is embedded in the stat information.
   190  	ispec.History
   191  }
   192  
   193  // Stat computes the ManifestStat for a given manifest blob. The provided
   194  // descriptor must refer to an OCI Manifest.
   195  func Stat(ctx context.Context, engine casext.Engine, manifestDescriptor ispec.Descriptor) (ManifestStat, error) {
   196  	var stat ManifestStat
   197  
   198  	if manifestDescriptor.MediaType != ispec.MediaTypeImageManifest {
   199  		return stat, errors.Errorf("stat: cannot stat a non-manifest descriptor: invalid media type '%s'", manifestDescriptor.MediaType)
   200  	}
   201  
   202  	// We have to get the actual manifest.
   203  	manifestBlob, err := engine.FromDescriptor(ctx, manifestDescriptor)
   204  	if err != nil {
   205  		return stat, err
   206  	}
   207  	manifest, ok := manifestBlob.Data.(ispec.Manifest)
   208  	if !ok {
   209  		// Should _never_ be reached.
   210  		return stat, errors.Errorf("[internal error] unknown manifest blob type: %s", manifestBlob.MediaType)
   211  	}
   212  
   213  	// Now get the config.
   214  	configBlob, err := engine.FromDescriptor(ctx, manifest.Config)
   215  	if err != nil {
   216  		return stat, errors.Wrap(err, "stat")
   217  	}
   218  	config, ok := configBlob.Data.(ispec.Image)
   219  	if !ok {
   220  		// Should _never_ be reached.
   221  		return stat, errors.Errorf("[internal error] unknown config blob type: %s", configBlob.MediaType)
   222  	}
   223  
   224  	// TODO: This should probably be moved into separate functions.
   225  
   226  	// Generate the history of the image. Because the config.History entries
   227  	// are in the same order as the manifest.Layer entries this is fairly
   228  	// simple. However, we only increment the layer index if a layer was
   229  	// actually generated by a history entry.
   230  	layerIdx := 0
   231  	for _, histEntry := range config.History {
   232  		info := historyStat{
   233  			History: histEntry,
   234  			DiffID:  "",
   235  			Layer:   nil,
   236  		}
   237  
   238  		// Only fill the other information and increment layerIdx if it's a
   239  		// non-empty layer.
   240  		if !histEntry.EmptyLayer {
   241  			info.DiffID = config.RootFS.DiffIDs[layerIdx].String()
   242  			info.Layer = &manifest.Layers[layerIdx]
   243  			layerIdx++
   244  		}
   245  
   246  		stat.History = append(stat.History, info)
   247  	}
   248  
   249  	return stat, nil
   250  }
   251  
   252  // GenerateBundleManifest creates and writes an mtree of the rootfs in the given
   253  // bundle path, using the supplied fsEval method
   254  func GenerateBundleManifest(mtreeName string, bundlePath string, fsEval mtree.FsEval) error {
   255  	mtreePath := filepath.Join(bundlePath, mtreeName+".mtree")
   256  	fullRootfsPath := filepath.Join(bundlePath, layer.RootfsName)
   257  
   258  	log.WithFields(log.Fields{
   259  		"keywords": MtreeKeywords,
   260  		"mtree":    mtreePath,
   261  	}).Debugf("umoci: generating mtree manifest")
   262  
   263  	log.Info("computing filesystem manifest ...")
   264  	dh, err := mtree.Walk(fullRootfsPath, nil, MtreeKeywords, fsEval)
   265  	if err != nil {
   266  		return errors.Wrap(err, "generate mtree spec")
   267  	}
   268  	log.Info("... done")
   269  
   270  	flags := os.O_CREATE | os.O_WRONLY | os.O_EXCL
   271  	fh, err := os.OpenFile(mtreePath, flags, 0644)
   272  	if err != nil {
   273  		return errors.Wrap(err, "open mtree")
   274  	}
   275  	defer fh.Close()
   276  
   277  	log.Debugf("umoci: saving mtree manifest")
   278  
   279  	if _, err := dh.WriteTo(fh); err != nil {
   280  		return errors.Wrap(err, "write mtree")
   281  	}
   282  
   283  	return nil
   284  }
   285  
   286  // ParseIdmapOptions sets up the mapping options for Meta, using
   287  // the arguments specified on the command line
   288  func ParseIdmapOptions(meta *Meta, ctx *cli.Context) error {
   289  	// We need to set mappings if we're in rootless mode.
   290  	meta.MapOptions.Rootless = ctx.Bool("rootless")
   291  	if meta.MapOptions.Rootless {
   292  		if !ctx.IsSet("uid-map") {
   293  			ctx.Set("uid-map", fmt.Sprintf("0:%d:1", os.Geteuid()))
   294  		}
   295  		if !ctx.IsSet("gid-map") {
   296  			ctx.Set("gid-map", fmt.Sprintf("0:%d:1", os.Getegid()))
   297  		}
   298  	}
   299  
   300  	for _, uidmap := range ctx.StringSlice("uid-map") {
   301  		idMap, err := idtools.ParseMapping(uidmap)
   302  		if err != nil {
   303  			return errors.Wrapf(err, "failure parsing --uid-map %s", uidmap)
   304  		}
   305  		meta.MapOptions.UIDMappings = append(meta.MapOptions.UIDMappings, idMap)
   306  	}
   307  	for _, gidmap := range ctx.StringSlice("gid-map") {
   308  		idMap, err := idtools.ParseMapping(gidmap)
   309  		if err != nil {
   310  			return errors.Wrapf(err, "failure parsing --gid-map %s", gidmap)
   311  		}
   312  		meta.MapOptions.GIDMappings = append(meta.MapOptions.GIDMappings, idMap)
   313  	}
   314  
   315  	log.WithFields(log.Fields{
   316  		"map.uid": meta.MapOptions.UIDMappings,
   317  		"map.gid": meta.MapOptions.GIDMappings,
   318  	}).Debugf("parsed mappings")
   319  
   320  	return nil
   321  }