...

Source file src/github.com/Microsoft/hcsshim/cmd/dmverity-vhd/main.go

Documentation: github.com/Microsoft/hcsshim/cmd/dmverity-vhd

     1  package main
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  	"os"
     7  	"path/filepath"
     8  
     9  	"github.com/google/go-containerregistry/pkg/authn"
    10  	"github.com/google/go-containerregistry/pkg/name"
    11  	v1 "github.com/google/go-containerregistry/pkg/v1"
    12  	"github.com/google/go-containerregistry/pkg/v1/daemon"
    13  	"github.com/google/go-containerregistry/pkg/v1/remote"
    14  	"github.com/google/go-containerregistry/pkg/v1/tarball"
    15  	log "github.com/sirupsen/logrus"
    16  	"github.com/urfave/cli"
    17  
    18  	"github.com/Microsoft/hcsshim/ext4/dmverity"
    19  	"github.com/Microsoft/hcsshim/ext4/tar2ext4"
    20  )
    21  
    22  const usage = `dmverity-vhd is a command line tool for creating LCOW layer VHDs with dm-verity hashes.`
    23  
    24  const (
    25  	usernameFlag      = "username"
    26  	passwordFlag      = "password"
    27  	imageFlag         = "image"
    28  	verboseFlag       = "verbose"
    29  	outputDirFlag     = "out-dir"
    30  	dockerFlag        = "docker"
    31  	tarballFlag       = "tarball"
    32  	hashDeviceVhdFlag = "hash-dev-vhd"
    33  	maxVHDSize        = dmverity.RecommendedVHDSizeGB
    34  )
    35  
    36  func init() {
    37  	log.SetFormatter(&log.TextFormatter{
    38  		DisableTimestamp: true,
    39  	})
    40  
    41  	log.SetOutput(os.Stdout)
    42  
    43  	log.SetLevel(log.WarnLevel)
    44  }
    45  
    46  func main() {
    47  	cli.VersionFlag = cli.BoolFlag{
    48  		Name: "version",
    49  	}
    50  
    51  	app := cli.NewApp()
    52  	app.Name = "dmverity-vhd"
    53  	app.Commands = []cli.Command{
    54  		createVHDCommand,
    55  		rootHashVHDCommand,
    56  	}
    57  	app.Usage = usage
    58  	app.Flags = []cli.Flag{
    59  		cli.BoolFlag{
    60  			Name:  verboseFlag + ",v",
    61  			Usage: "Optional: verbose output",
    62  		},
    63  		cli.BoolFlag{
    64  			Name:  dockerFlag + ",d",
    65  			Usage: "Optional: use local docker daemon",
    66  		},
    67  		cli.StringFlag{
    68  			Name:  tarballFlag + ",t",
    69  			Usage: "Optional: path to tarball containing image info",
    70  		},
    71  	}
    72  
    73  	if err := app.Run(os.Args); err != nil {
    74  		_, _ = fmt.Fprintln(os.Stderr, err)
    75  		os.Exit(1)
    76  	}
    77  }
    78  
    79  func fetchImageLayers(ctx *cli.Context) (layers []v1.Layer, err error) {
    80  	image := ctx.String(imageFlag)
    81  	tarballPath := ctx.GlobalString(tarballFlag)
    82  	ref, err := name.ParseReference(image)
    83  	if err != nil {
    84  		return nil, fmt.Errorf("failed to parse image reference %s: %w", image, err)
    85  	}
    86  
    87  	dockerDaemon := ctx.GlobalBool(dockerFlag)
    88  
    89  	// error check to make sure docker and tarball are not both defined
    90  	if dockerDaemon && tarballPath != "" {
    91  		return nil, errors.New("cannot use both docker and tarball for image source")
    92  	}
    93  
    94  	// by default, using remote as source
    95  	var img v1.Image
    96  	if tarballPath != "" {
    97  		// create a tag and search the tarball for the image specified
    98  		var imageNameAndTag name.Tag
    99  		imageNameAndTag, err = name.NewTag(image)
   100  		if err != nil {
   101  			return nil, fmt.Errorf("failed to failed to create a tag to search tarball for %s: %w", image, err)
   102  		}
   103  		// if only an image name is provided and not a tag, the default is "latest"
   104  		img, err = tarball.ImageFromPath(tarballPath, &imageNameAndTag)
   105  	} else if dockerDaemon {
   106  		// use the unbuffered opener, the tradeoff being the image will stream as needed
   107  		// so it is slower but much more memory efficient
   108  		var opts []daemon.Option
   109  		opt := daemon.WithUnbufferedOpener()
   110  		opts = append(opts, opt)
   111  
   112  		img, err = daemon.Image(ref, opts...)
   113  	} else {
   114  		var remoteOpts []remote.Option
   115  		if ctx.IsSet(usernameFlag) && ctx.IsSet(passwordFlag) {
   116  			auth := authn.Basic{
   117  				Username: ctx.String(usernameFlag),
   118  				Password: ctx.String(passwordFlag),
   119  			}
   120  			authConf, err := auth.Authorization()
   121  			if err != nil {
   122  				return nil, fmt.Errorf("failed to set remote: %w", err)
   123  			}
   124  			log.Debug("using basic auth")
   125  			authOpt := remote.WithAuth(authn.FromConfig(*authConf))
   126  			remoteOpts = append(remoteOpts, authOpt)
   127  		}
   128  
   129  		img, err = remote.Image(ref, remoteOpts...)
   130  	}
   131  	if err != nil {
   132  		return nil, fmt.Errorf("unable to fetch image %q, make sure it exists: %w", image, err)
   133  	}
   134  	conf, _ := img.ConfigName()
   135  	log.Debugf("Image id: %s", conf.String())
   136  	return img.Layers()
   137  }
   138  
   139  var createVHDCommand = cli.Command{
   140  	Name:  "create",
   141  	Usage: "creates LCOW layer VHDs inside the output directory with dm-verity super block and merkle tree appended at the end",
   142  	Flags: []cli.Flag{
   143  		cli.StringFlag{
   144  			Name:     imageFlag + ",i",
   145  			Usage:    "Required: container image reference",
   146  			Required: true,
   147  		},
   148  		cli.StringFlag{
   149  			Name:     outputDirFlag + ",o",
   150  			Usage:    "Required: output directory path",
   151  			Required: true,
   152  		},
   153  		cli.StringFlag{
   154  			Name:  usernameFlag + ",u",
   155  			Usage: "Optional: custom registry username",
   156  		},
   157  		cli.StringFlag{
   158  			Name:  passwordFlag + ",p",
   159  			Usage: "Optional: custom registry password",
   160  		},
   161  		cli.BoolFlag{
   162  			Name:  hashDeviceVhdFlag + ",hdv",
   163  			Usage: "Optional: save hash-device as a VHD",
   164  		},
   165  	},
   166  	Action: func(ctx *cli.Context) error {
   167  		verbose := ctx.GlobalBool(verboseFlag)
   168  		if verbose {
   169  			log.SetLevel(log.DebugLevel)
   170  		}
   171  
   172  		layers, err := fetchImageLayers(ctx)
   173  		if err != nil {
   174  			return fmt.Errorf("failed to fetch image layers: %w", err)
   175  		}
   176  
   177  		outDir := ctx.String(outputDirFlag)
   178  		if _, err := os.Stat(outDir); os.IsNotExist(err) {
   179  			log.Debugf("creating output directory %q", outDir)
   180  			if err := os.MkdirAll(outDir, 0755); err != nil {
   181  				return fmt.Errorf("failed to create output directory %s: %w", outDir, err)
   182  			}
   183  		}
   184  
   185  		log.Debug("creating layer VHDs with dm-verity")
   186  		for layerNumber, layer := range layers {
   187  			if err := createVHD(layerNumber, layer, ctx.String(outputDirFlag), ctx.Bool(hashDeviceVhdFlag)); err != nil {
   188  				return err
   189  			}
   190  		}
   191  		return nil
   192  	},
   193  }
   194  
   195  func createVHD(layerNumber int, layer v1.Layer, outDir string, verityHashDev bool) error {
   196  	diffID, err := layer.DiffID()
   197  	if err != nil {
   198  		return fmt.Errorf("failed to read layer diff: %w", err)
   199  	}
   200  
   201  	log.WithFields(log.Fields{
   202  		"layerNumber": layerNumber,
   203  		"layerDiff":   diffID.String(),
   204  	}).Debug("converting tar to layer VHD")
   205  
   206  	rc, err := layer.Uncompressed()
   207  	if err != nil {
   208  		return fmt.Errorf("failed to uncompress layer %s: %w", diffID.String(), err)
   209  	}
   210  	defer rc.Close()
   211  
   212  	vhdPath := filepath.Join(outDir, diffID.Hex+".vhd")
   213  	out, err := os.Create(vhdPath)
   214  	if err != nil {
   215  		return fmt.Errorf("failed to create layer vhd file %s: %w", vhdPath, err)
   216  	}
   217  	defer out.Close()
   218  
   219  	opts := []tar2ext4.Option{
   220  		tar2ext4.ConvertWhiteout,
   221  		tar2ext4.MaximumDiskSize(maxVHDSize),
   222  	}
   223  	if !verityHashDev {
   224  		opts = append(opts, tar2ext4.AppendDMVerity)
   225  	}
   226  	if err := tar2ext4.Convert(rc, out, opts...); err != nil {
   227  		return fmt.Errorf("failed to convert tar to ext4: %w", err)
   228  	}
   229  	if verityHashDev {
   230  		hashDevPath := filepath.Join(outDir, diffID.Hex+".hash-dev.vhd")
   231  		hashDev, err := os.Create(hashDevPath)
   232  		if err != nil {
   233  			return fmt.Errorf("failed to create hash device VHD file: %w", err)
   234  		}
   235  		defer hashDev.Close()
   236  
   237  		if err := dmverity.ComputeAndWriteHashDevice(out, hashDev); err != nil {
   238  			return err
   239  		}
   240  		if err := tar2ext4.ConvertToVhd(hashDev); err != nil {
   241  			return err
   242  		}
   243  		fmt.Fprintf(os.Stdout, "hash device created at %s\n", hashDevPath)
   244  	}
   245  	if err := tar2ext4.ConvertToVhd(out); err != nil {
   246  		return fmt.Errorf("failed to append VHD footer: %w", err)
   247  	}
   248  	fmt.Fprintf(os.Stdout, "Layer VHD created at %s\n", vhdPath)
   249  	return nil
   250  }
   251  
   252  var rootHashVHDCommand = cli.Command{
   253  	Name:  "roothash",
   254  	Usage: "compute root hashes for each LCOW layer VHD",
   255  	Flags: []cli.Flag{
   256  		cli.StringFlag{
   257  			Name:     imageFlag + ",i",
   258  			Usage:    "Required: container image reference",
   259  			Required: true,
   260  		},
   261  		cli.StringFlag{
   262  			Name:  usernameFlag + ",u",
   263  			Usage: "Optional: custom registry username",
   264  		},
   265  		cli.StringFlag{
   266  			Name:  passwordFlag + ",p",
   267  			Usage: "Optional: custom registry password",
   268  		},
   269  	},
   270  	Action: func(ctx *cli.Context) error {
   271  		verbose := ctx.GlobalBool(verboseFlag)
   272  		if verbose {
   273  			log.SetLevel(log.DebugLevel)
   274  		}
   275  
   276  		layers, err := fetchImageLayers(ctx)
   277  		if err != nil {
   278  			return fmt.Errorf("failed to fetch image layers: %w", err)
   279  		}
   280  		log.Debugf("%d layers found", len(layers))
   281  
   282  		convertFunc := func(layer v1.Layer) (string, error) {
   283  			rc, err := layer.Uncompressed()
   284  			if err != nil {
   285  				return "", err
   286  			}
   287  			defer rc.Close()
   288  
   289  			hash, err := tar2ext4.ConvertAndComputeRootDigest(rc)
   290  			if err != nil {
   291  				return "", err
   292  			}
   293  			return hash, err
   294  		}
   295  
   296  		for layerNumber, layer := range layers {
   297  			diffID, err := layer.DiffID()
   298  			if err != nil {
   299  				return fmt.Errorf("failed to read layer diff: %w", err)
   300  			}
   301  			log.WithFields(log.Fields{
   302  				"layerNumber": layerNumber,
   303  				"layerDiff":   diffID.String(),
   304  			}).Debug("uncompressed layer")
   305  
   306  			hash, err := convertFunc(layer)
   307  			if err != nil {
   308  				return fmt.Errorf("failed to compute root digest: %w", err)
   309  			}
   310  			fmt.Fprintf(os.Stdout, "Layer %d root hash: %s\n", layerNumber, hash)
   311  		}
   312  		return nil
   313  	},
   314  }
   315  

View as plain text