github.com/opencontainers/umoci@v0.4.8-0.20240508124516-656e4836fb0d/utils.go (about)

     1  /*
     2   * umoci: Umoci Modifies Open Containers' Images
     3   * Copyright (C) 2016-2020 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  	"context"
    23  	"encoding/json"
    24  	"fmt"
    25  	"io"
    26  	"os"
    27  	"path/filepath"
    28  	"strings"
    29  	"text/tabwriter"
    30  
    31  	"github.com/apex/log"
    32  	"github.com/docker/go-units"
    33  	ispec "github.com/opencontainers/image-spec/specs-go/v1"
    34  	"github.com/opencontainers/umoci/oci/casext"
    35  	igen "github.com/opencontainers/umoci/oci/config/generate"
    36  	"github.com/opencontainers/umoci/oci/layer"
    37  	"github.com/opencontainers/umoci/pkg/idtools"
    38  	"github.com/pkg/errors"
    39  	"github.com/urfave/cli"
    40  	"github.com/vbatts/go-mtree"
    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  	// WhiteoutMode indicates what style of whiteout was written to disk
    94  	// when this filesystem was extracted.
    95  	WhiteoutMode layer.WhiteoutMode `json:"whiteout_mode"`
    96  }
    97  
    98  // WriteTo writes a JSON-serialised version of Meta to the given io.Writer.
    99  func (m Meta) WriteTo(w io.Writer) (int64, error) {
   100  	buf := new(bytes.Buffer)
   101  	err := json.NewEncoder(io.MultiWriter(buf, w)).Encode(m)
   102  	return int64(buf.Len()), err
   103  }
   104  
   105  // WriteBundleMeta writes an umoci.json file to the given bundle path.
   106  func WriteBundleMeta(bundle string, meta Meta) error {
   107  	fh, err := os.Create(filepath.Join(bundle, MetaName))
   108  	if err != nil {
   109  		return errors.Wrap(err, "create metadata")
   110  	}
   111  	defer fh.Close()
   112  
   113  	_, err = meta.WriteTo(fh)
   114  	return errors.Wrap(err, "write metadata")
   115  }
   116  
   117  // ReadBundleMeta reads and parses the umoci.json file from a given bundle path.
   118  func ReadBundleMeta(bundle string) (Meta, error) {
   119  	var meta Meta
   120  
   121  	fh, err := os.Open(filepath.Join(bundle, MetaName))
   122  	if err != nil {
   123  		return meta, errors.Wrap(err, "open metadata")
   124  	}
   125  	defer fh.Close()
   126  
   127  	err = json.NewDecoder(fh).Decode(&meta)
   128  	if meta.Version != MetaVersion {
   129  		if err == nil {
   130  			err = fmt.Errorf("unsupported umoci.json version: %s", meta.Version)
   131  		}
   132  	}
   133  	return meta, errors.Wrap(err, "decode metadata")
   134  }
   135  
   136  // ManifestStat has information about a given OCI manifest.
   137  // TODO: Implement support for manifest lists, this should also be able to
   138  //
   139  //	contain stat information for a list of manifests.
   140  type ManifestStat struct {
   141  	// TODO: Flesh this out. Currently it's only really being used to get an
   142  	//       equivalent of docker-history(1). We really need to add more
   143  	//       information about it.
   144  
   145  	// History stores the history information for the manifest.
   146  	History []historyStat `json:"history"`
   147  }
   148  
   149  // Format formats a ManifestStat using the default formatting, and writes the
   150  // result to the given writer.
   151  // TODO: This should really be implemented in a way that allows for users to
   152  //
   153  //	define their own custom templates for different blocks (meaning that
   154  //	this should use text/template rather than using tabwriters manually.
   155  func (ms ManifestStat) Format(w io.Writer) error {
   156  	// Output history information.
   157  	tw := tabwriter.NewWriter(w, 4, 2, 1, ' ', 0)
   158  	fmt.Fprintf(tw, "LAYER\tCREATED\tCREATED BY\tSIZE\tCOMMENT\n")
   159  	for _, histEntry := range ms.History {
   160  		var (
   161  			created   = strings.Replace(histEntry.Created.Format(igen.ISO8601), "\t", " ", -1)
   162  			createdBy = strings.Replace(histEntry.CreatedBy, "\t", " ", -1)
   163  			comment   = strings.Replace(histEntry.Comment, "\t", " ", -1)
   164  			layerID   = "<none>"
   165  			size      = "<none>"
   166  		)
   167  
   168  		if !histEntry.EmptyLayer {
   169  			layerID = histEntry.Layer.Digest.String()
   170  			size = units.HumanSize(float64(histEntry.Layer.Size))
   171  		}
   172  
   173  		// TODO: We need to truncate some of the fields.
   174  		fmt.Fprintf(tw, "%s\t%s\t%s\t%s\t%s\n", layerID, created, createdBy, size, comment)
   175  	}
   176  	return tw.Flush()
   177  }
   178  
   179  // historyStat contains information about a single entry in the history of a
   180  // manifest. This is essentially equivalent to a single record from
   181  // docker-history(1).
   182  type historyStat struct {
   183  	// Layer is the descriptor referencing where the layer is stored. If it is
   184  	// nil, then this entry is an empty_layer (and thus doesn't have a backing
   185  	// diff layer).
   186  	Layer *ispec.Descriptor `json:"layer"`
   187  
   188  	// DiffID is an additional piece of information to Layer. It stores the
   189  	// DiffID of the given layer corresponding to the history entry. If DiffID
   190  	// is "", then this entry is an empty_layer.
   191  	DiffID string `json:"diff_id"`
   192  
   193  	// History is embedded in the stat information.
   194  	ispec.History
   195  }
   196  
   197  // Stat computes the ManifestStat for a given manifest blob. The provided
   198  // descriptor must refer to an OCI Manifest.
   199  func Stat(ctx context.Context, engine casext.Engine, manifestDescriptor ispec.Descriptor) (ManifestStat, error) {
   200  	var stat ManifestStat
   201  
   202  	if manifestDescriptor.MediaType != ispec.MediaTypeImageManifest {
   203  		return stat, errors.Errorf("stat: cannot stat a non-manifest descriptor: invalid media type '%s'", manifestDescriptor.MediaType)
   204  	}
   205  
   206  	// We have to get the actual manifest.
   207  	manifestBlob, err := engine.FromDescriptor(ctx, manifestDescriptor)
   208  	if err != nil {
   209  		return stat, err
   210  	}
   211  	manifest, ok := manifestBlob.Data.(ispec.Manifest)
   212  	if !ok {
   213  		// Should _never_ be reached.
   214  		return stat, errors.Errorf("[internal error] unknown manifest blob type: %s", manifestBlob.Descriptor.MediaType)
   215  	}
   216  
   217  	// Now get the config.
   218  	configBlob, err := engine.FromDescriptor(ctx, manifest.Config)
   219  	if err != nil {
   220  		return stat, errors.Wrap(err, "stat")
   221  	}
   222  	config, ok := configBlob.Data.(ispec.Image)
   223  	if !ok {
   224  		// Should _never_ be reached.
   225  		return stat, errors.Errorf("[internal error] unknown config blob type: %s", configBlob.Descriptor.MediaType)
   226  	}
   227  
   228  	// TODO: This should probably be moved into separate functions.
   229  
   230  	// Generate the history of the image. Because the config.History entries
   231  	// are in the same order as the manifest.Layer entries this is fairly
   232  	// simple. However, we only increment the layer index if a layer was
   233  	// actually generated by a history entry.
   234  	layerIdx := 0
   235  	for _, histEntry := range config.History {
   236  		info := historyStat{
   237  			History: histEntry,
   238  			DiffID:  "",
   239  			Layer:   nil,
   240  		}
   241  
   242  		// Only fill the other information and increment layerIdx if it's a
   243  		// non-empty layer.
   244  		if !histEntry.EmptyLayer {
   245  			info.DiffID = config.RootFS.DiffIDs[layerIdx].String()
   246  			info.Layer = &manifest.Layers[layerIdx]
   247  			layerIdx++
   248  		}
   249  
   250  		stat.History = append(stat.History, info)
   251  	}
   252  
   253  	return stat, nil
   254  }
   255  
   256  // GenerateBundleManifest creates and writes an mtree of the rootfs in the given
   257  // bundle path, using the supplied fsEval method
   258  func GenerateBundleManifest(mtreeName string, bundlePath string, fsEval mtree.FsEval) error {
   259  	mtreePath := filepath.Join(bundlePath, mtreeName+".mtree")
   260  	fullRootfsPath := filepath.Join(bundlePath, layer.RootfsName)
   261  
   262  	log.WithFields(log.Fields{
   263  		"keywords": MtreeKeywords,
   264  		"mtree":    mtreePath,
   265  	}).Debugf("umoci: generating mtree manifest")
   266  
   267  	log.Info("computing filesystem manifest ...")
   268  	dh, err := mtree.Walk(fullRootfsPath, nil, MtreeKeywords, fsEval)
   269  	if err != nil {
   270  		return errors.Wrap(err, "generate mtree spec")
   271  	}
   272  	log.Info("... done")
   273  
   274  	flags := os.O_CREATE | os.O_WRONLY | os.O_EXCL
   275  	fh, err := os.OpenFile(mtreePath, flags, 0644)
   276  	if err != nil {
   277  		return errors.Wrap(err, "open mtree")
   278  	}
   279  	defer fh.Close()
   280  
   281  	log.Debugf("umoci: saving mtree manifest")
   282  
   283  	if _, err := dh.WriteTo(fh); err != nil {
   284  		return errors.Wrap(err, "write mtree")
   285  	}
   286  
   287  	return nil
   288  }
   289  
   290  // ParseIdmapOptions sets up the mapping options for Meta, using
   291  // the arguments specified on the command line
   292  func ParseIdmapOptions(meta *Meta, ctx *cli.Context) error {
   293  	// We need to set mappings if we're in rootless mode.
   294  	meta.MapOptions.Rootless = ctx.Bool("rootless")
   295  	if meta.MapOptions.Rootless {
   296  		if !ctx.IsSet("uid-map") {
   297  			if err := ctx.Set("uid-map", fmt.Sprintf("0:%d:1", os.Geteuid())); err != nil {
   298  				// Should _never_ be reached.
   299  				return errors.Wrap(err, "[internal error] failure auto-setting rootless --uid-map")
   300  			}
   301  		}
   302  		if !ctx.IsSet("gid-map") {
   303  			if err := ctx.Set("gid-map", fmt.Sprintf("0:%d:1", os.Getegid())); err != nil {
   304  				// Should _never_ be reached.
   305  				return errors.Wrap(err, "[internal error] failure auto-setting rootless --gid-map")
   306  			}
   307  		}
   308  	}
   309  
   310  	for _, uidmap := range ctx.StringSlice("uid-map") {
   311  		idMap, err := idtools.ParseMapping(uidmap)
   312  		if err != nil {
   313  			return errors.Wrapf(err, "failure parsing --uid-map %s", uidmap)
   314  		}
   315  		meta.MapOptions.UIDMappings = append(meta.MapOptions.UIDMappings, idMap)
   316  	}
   317  	for _, gidmap := range ctx.StringSlice("gid-map") {
   318  		idMap, err := idtools.ParseMapping(gidmap)
   319  		if err != nil {
   320  			return errors.Wrapf(err, "failure parsing --gid-map %s", gidmap)
   321  		}
   322  		meta.MapOptions.GIDMappings = append(meta.MapOptions.GIDMappings, idMap)
   323  	}
   324  
   325  	log.WithFields(log.Fields{
   326  		"map.uid": meta.MapOptions.UIDMappings,
   327  		"map.gid": meta.MapOptions.GIDMappings,
   328  	}).Debugf("parsed mappings")
   329  
   330  	return nil
   331  }