github.com/opencontainers/umoci@v0.4.8-0.20240508124516-656e4836fb0d/cmd/umoci/repack.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 main
    19  
    20  import (
    21  	"context"
    22  	"fmt"
    23  	"time"
    24  
    25  	"github.com/apex/log"
    26  	ispec "github.com/opencontainers/image-spec/specs-go/v1"
    27  	"github.com/opencontainers/umoci"
    28  	"github.com/opencontainers/umoci/mutate"
    29  	"github.com/opencontainers/umoci/oci/cas/dir"
    30  	"github.com/opencontainers/umoci/oci/casext"
    31  	igen "github.com/opencontainers/umoci/oci/config/generate"
    32  	"github.com/opencontainers/umoci/pkg/mtreefilter"
    33  	"github.com/pkg/errors"
    34  	"github.com/urfave/cli"
    35  )
    36  
    37  var repackCommand = uxHistory(cli.Command{
    38  	Name:  "repack",
    39  	Usage: "repacks an OCI runtime bundle into a reference",
    40  	ArgsUsage: `--image <image-path>[:<new-tag>] <bundle>
    41  
    42  Where "<image-path>" is the path to the OCI image, "<new-tag>" is the name of
    43  the tag that the new image will be saved as (if not specified, defaults to
    44  "latest"), and "<bundle>" is the bundle from which to generate the required
    45  layers.
    46  
    47  The "<image-path>" MUST be the same image that was used to create "<bundle>"
    48  (using umoci-unpack(1)). Otherwise umoci will not be able to modify the
    49  original manifest to add the diff layer.
    50  
    51  All uid-map and gid-map settings are automatically loaded from the bundle
    52  metadata (which is generated by umoci-unpack(1)) so if you unpacked an image
    53  using a particular mapping then the same mapping will be used to generate the
    54  new layer.
    55  
    56  It should be noted that this is not the same as oci-create-layer because it
    57  uses go-mtree to create diff layers from runtime bundles unpacked with
    58  umoci-unpack(1). In addition, it modifies the image so that all of the relevant
    59  manifest and configuration information uses the new diff atop the old manifest.`,
    60  
    61  	// repack creates a new image, with a given tag.
    62  	Category: "image",
    63  
    64  	Flags: []cli.Flag{
    65  		cli.StringSliceFlag{
    66  			Name:  "mask-path",
    67  			Usage: "set of path prefixes in which deltas will be ignored when generating new layers",
    68  		},
    69  		cli.BoolFlag{
    70  			Name:  "no-mask-volumes",
    71  			Usage: "do not add the Config.Volumes of the image to the set of masked paths",
    72  		},
    73  		cli.BoolFlag{
    74  			Name:  "refresh-bundle",
    75  			Usage: "update the bundle metadata to reflect the packed rootfs",
    76  		},
    77  	},
    78  
    79  	Action: repack,
    80  
    81  	Before: func(ctx *cli.Context) error {
    82  		if ctx.NArg() != 1 {
    83  			return errors.Errorf("invalid number of positional arguments: expected <bundle>")
    84  		}
    85  		if ctx.Args().First() == "" {
    86  			return errors.Errorf("bundle path cannot be empty")
    87  		}
    88  		ctx.App.Metadata["bundle"] = ctx.Args().First()
    89  		return nil
    90  	},
    91  })
    92  
    93  func repack(ctx *cli.Context) error {
    94  	imagePath := ctx.App.Metadata["--image-path"].(string)
    95  	tagName := ctx.App.Metadata["--image-tag"].(string)
    96  	bundlePath := ctx.App.Metadata["bundle"].(string)
    97  
    98  	// Read the metadata first.
    99  	meta, err := umoci.ReadBundleMeta(bundlePath)
   100  	if err != nil {
   101  		return errors.Wrap(err, "read umoci.json metadata")
   102  	}
   103  
   104  	log.WithFields(log.Fields{
   105  		"version":     meta.Version,
   106  		"from":        meta.From,
   107  		"map_options": meta.MapOptions,
   108  	}).Debugf("umoci: loaded Meta metadata")
   109  
   110  	if meta.From.Descriptor().MediaType != ispec.MediaTypeImageManifest {
   111  		return errors.Wrap(fmt.Errorf("descriptor does not point to ispec.MediaTypeImageManifest: not implemented: %s", meta.From.Descriptor().MediaType), "invalid saved from descriptor")
   112  	}
   113  
   114  	// Get a reference to the CAS.
   115  	engine, err := dir.Open(imagePath)
   116  	if err != nil {
   117  		return errors.Wrap(err, "open CAS")
   118  	}
   119  	engineExt := casext.NewEngine(engine)
   120  	defer engine.Close()
   121  
   122  	// Create the mutator.
   123  	mutator, err := mutate.New(engineExt, meta.From)
   124  	if err != nil {
   125  		return errors.Wrap(err, "create mutator for base image")
   126  	}
   127  
   128  	// We need to mask config.Volumes.
   129  	config, err := mutator.Config(context.Background())
   130  	if err != nil {
   131  		return errors.Wrap(err, "get config")
   132  	}
   133  
   134  	maskedPaths := ctx.StringSlice("mask-path")
   135  	if !ctx.Bool("no-mask-volumes") {
   136  		for v := range config.Config.Volumes {
   137  			maskedPaths = append(maskedPaths, v)
   138  		}
   139  	}
   140  
   141  	imageMeta, err := mutator.Meta(context.Background())
   142  	if err != nil {
   143  		return errors.Wrap(err, "get image metadata")
   144  	}
   145  
   146  	var history *ispec.History
   147  	if !ctx.Bool("no-history") {
   148  		created := time.Now()
   149  		history = &ispec.History{
   150  			Author:     imageMeta.Author,
   151  			Comment:    "",
   152  			Created:    &created,
   153  			CreatedBy:  "umoci repack", // XXX: Should we append argv to this?
   154  			EmptyLayer: false,
   155  		}
   156  
   157  		if ctx.IsSet("history.author") {
   158  			history.Author = ctx.String("history.author")
   159  		}
   160  		if ctx.IsSet("history.comment") {
   161  			history.Comment = ctx.String("history.comment")
   162  		}
   163  		if ctx.IsSet("history.created") {
   164  			created, err := time.Parse(igen.ISO8601, ctx.String("history.created"))
   165  			if err != nil {
   166  				return errors.Wrap(err, "parsing --history.created")
   167  			}
   168  			history.Created = &created
   169  		}
   170  		if ctx.IsSet("history.created_by") {
   171  			history.CreatedBy = ctx.String("history.created_by")
   172  		}
   173  	}
   174  
   175  	filters := []mtreefilter.FilterFunc{
   176  		mtreefilter.MaskFilter(maskedPaths),
   177  	}
   178  
   179  	return umoci.Repack(engineExt, tagName, bundlePath, meta, history, filters, ctx.Bool("refresh-bundle"), mutator)
   180  }