Help us shape the future of Go! Take the 2026 Go Developer Survey

commands

package
v0.19.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 27, 2026 License: Apache-2.0 Imports: 55 Imported by: 0

Documentation

Index

Constants

This section is empty.

Variables

View Source
var ConvertCommand = &cli.Command{
	Name:      "convert",
	Usage:     "convert an image",
	ArgsUsage: "[flags] <source_ref> <target_ref>...",
	Description: `Convert an image format.

e.g., 'ctr-remote convert --estargz --oci example.com/foo:orig example.com/foo:esgz'

Use '--platform' to define the output platform.
When '--all-platforms' is given all images in a manifest list must be available.
`,
	Flags: []cli.Flag{

		&cli.BoolFlag{
			Name:  "estargz",
			Usage: "convert legacy tar(.gz) layers to eStargz for lazy pulling. Should be used in conjunction with '--oci'",
		},
		&cli.StringFlag{
			Name:  "estargz-record-in",
			Usage: "Read 'ctr-remote optimize --record-out=<FILE>' record file",
		},
		&cli.IntFlag{
			Name:  "estargz-compression-level",
			Usage: "eStargz compression level",
			Value: gzip.BestCompression,
		},
		&cli.IntFlag{
			Name:  "estargz-chunk-size",
			Usage: "eStargz chunk size",
			Value: 0,
		},
		&cli.IntFlag{
			Name:  "estargz-min-chunk-size",
			Usage: "The minimal number of bytes of data must be written in one gzip stream. Note that this adds a TOC property that old reader doesn't understand.",
			Value: 0,
		},
		&cli.IntFlag{
			Name:  "estargz-parallelism",
			Usage: "Number of workers used to build the layer. Pinning it makes builds reproducible across machines regardless of CPU count. 0 (default) uses GOMAXPROCS; 1 forces a sequential build. Has no effect with --estargz-min-chunk-size.",
			Value: 0,
		},
		&cli.BoolFlag{
			Name:  "estargz-external-toc",
			Usage: "Separate TOC JSON into another image (called \"TOC image\"). The name of TOC image is the original + \"-esgztoc\" suffix. Both eStargz and the TOC image should be pushed to the same registry. stargz-snapshotter refers to the TOC image when it pulls the result eStargz image.",
		},
		&cli.BoolFlag{
			Name:  "estargz-keep-diff-id",
			Usage: "convert to esgz without changing diffID (cannot be used in conjunction with '--estargz-record-in'. must be specified with '--estargz-external-toc')",
		},
		&cli.StringFlag{
			Name:    "estargz-gzip-helper",
			Aliases: []string{"GH"},
			Usage:   "Helper command for decompressing layers compressed with gzip. Options: pigz, igzip, or gzip.",
			Value:   "",
		},

		&cli.BoolFlag{
			Name:  "zstdchunked",
			Usage: "use zstd compression instead of gzip (a.k.a zstd:chunked). Must be used in conjunction with '--oci'.",
		},
		&cli.StringFlag{
			Name:  "zstdchunked-record-in",
			Usage: "Read 'ctr-remote optimize --record-out=<FILE>' record file",
		},
		&cli.IntFlag{
			Name:  "zstdchunked-compression-level",
			Usage: "zstd:chunked compression level",
			Value: 3,
		},
		&cli.IntFlag{
			Name:  "zstdchunked-chunk-size",
			Usage: "zstd:chunked chunk size",
			Value: 0,
		},

		&cli.BoolFlag{
			Name:  "uncompress",
			Usage: "convert tar.gz layers to uncompressed tar layers",
		},
		&cli.BoolFlag{
			Name:  "oci",
			Usage: "convert Docker media types to OCI media types",
		},

		&cli.StringSliceFlag{
			Name:  "platform",
			Usage: "Convert content for a specific platform",
		},
		&cli.BoolFlag{
			Name:  "all-platforms",
			Usage: "Convert content for all platforms",
		},
	},
	Action: func(ctx gocontext.Context, context *cli.Command) error {
		var (
			convertOpts = []converter.Opt{}
		)
		srcRef := context.Args().Get(0)
		targetRef := context.Args().Get(1)
		if srcRef == "" || targetRef == "" {
			return errors.New("src and target image need to be specified")
		}

		var platformMC platforms.MatchComparer
		if context.Bool("all-platforms") {
			platformMC = platforms.All
		} else {
			if pss := context.StringSlice("platform"); len(pss) > 0 {
				var all []ocispec.Platform
				for _, ps := range pss {
					p, err := platforms.Parse(ps)
					if err != nil {
						return fmt.Errorf("invalid platform %q: %w", ps, err)
					}
					all = append(all, p)
				}
				platformMC = platforms.Ordered(all...)
			} else {
				platformMC = platforms.DefaultStrict()
			}
		}
		convertOpts = append(convertOpts, converter.WithPlatform(platformMC))

		var layerConvertFunc converter.ConvertFunc
		var finalize func(ctx gocontext.Context, cs content.Store, ref string, desc *ocispec.Descriptor) (*images.Image, error)
		if context.Bool("estargz") {
			esgzOpts, err := getESGZConvertOpts(context)
			if err != nil {
				return err
			}
			if context.Bool("estargz-external-toc") {
				if !context.Bool("estargz-keep-diff-id") {
					layerConvertFunc, finalize = esgzexternaltocconvert.LayerConvertFunc(esgzOpts, context.Int("estargz-compression-level"))
				} else {
					if context.String("estargz-record-in") != "" {
						return fmt.Errorf("option --estargz-keep-diff-id conflicts with --estargz-record-in")
					}
					layerConvertFunc, finalize = esgzexternaltocconvert.LayerConvertLossLessFunc(esgzexternaltocconvert.LayerConvertLossLessConfig{
						CompressionLevel: context.Int("estargz-compression-level"),
						ChunkSize:        context.Int("estargz-chunk-size"),
						MinChunkSize:     context.Int("estargz-min-chunk-size"),
					})
				}
			} else {
				if context.Bool("estargz-keep-diff-id") {
					return fmt.Errorf("option --estargz-keep-diff-id must be used with --estargz-external-toc")
				}
				layerConvertFunc = estargzconvert.LayerConvertFunc(esgzOpts...)
			}
			if !context.Bool("oci") {
				log.L.Warn("option --estargz should be used in conjunction with --oci")
			}
			if context.Bool("uncompress") {
				return errors.New("option --estargz conflicts with --uncompress")
			}
			if context.Bool("zstdchunked") {
				return errors.New("option --estargz conflicts with --zstdchunked")
			}
		}

		if context.Bool("zstdchunked") {
			esgzOpts, err := getZstdchunkedConvertOpts(context)
			if err != nil {
				return err
			}
			layerConvertFunc = zstdchunkedconvert.LayerConvertFuncWithCompressionLevel(
				zstd.EncoderLevelFromZstd(context.Int("zstdchunked-compression-level")), esgzOpts...)
			if !context.Bool("oci") {
				return errors.New("option --zstdchunked must be used in conjunction with --oci")
			}
			if context.Bool("uncompress") {
				return errors.New("option --zstdchunked conflicts with --uncompress")
			}
		}

		if context.Bool("uncompress") {
			layerConvertFunc = uncompress.LayerConvertFunc
		}

		if layerConvertFunc == nil {
			return errors.New("specify layer converter")
		}
		convertOpts = append(convertOpts, converter.WithLayerConvertFunc(layerConvertFunc))

		if context.Bool("oci") {
			convertOpts = append(convertOpts, converter.WithDockerToOCI(true))
		}

		client, ctx, cancel, err := commands.NewClient(ctx, context)
		if err != nil {
			return err
		}
		defer cancel()

		ctx, done, err := client.WithLease(ctx)
		if err != nil {
			return err
		}
		defer done(ctx)

		sigCh := make(chan os.Signal, 1)
		signal.Notify(sigCh, os.Interrupt)
		go func() {

			select {
			case s := <-sigCh:
				log.G(ctx).Infof("Got %v", s)
				cancel()
			case <-ctx.Done():
			}
		}()
		newImg, err := converter.Convert(ctx, client, targetRef, srcRef, convertOpts...)
		if err != nil {
			return err
		}
		if finalize != nil {
			newI, err := finalize(ctx, client.ContentStore(), targetRef, &newImg.Target)
			if err != nil {
				return err
			}
			is := client.ImageService()
			_ = is.Delete(ctx, newI.Name)
			finimg, err := is.Create(ctx, *newI)
			if err != nil {
				return err
			}
			fmt.Fprintln(context.Root().Writer, "extra image:", finimg.Name)
		}
		fmt.Fprintln(context.Root().Writer, newImg.Target.Digest.String())
		return nil
	},
}

ConvertCommand converts an image

View Source
var FanotifyCommand = &cli.Command{
	Name:   "fanotify",
	Hidden: true,
	Action: func(_ gocontext.Context, context *cli.Command) error {
		target := context.Args().Get(0)
		if target == "" {
			return fmt.Errorf("target must be specified")
		}
		return service.Serve(target, os.Stdin, os.Stdout)
	},
}

FanotifyCommand notifies filesystem event under the specified directory.

View Source
var GetTOCDigestCommand = &cli.Command{
	Name:      "get-toc-digest",
	Usage:     "get the digest of TOC of a layer",
	ArgsUsage: "<layer digest>",
	Flags: []cli.Flag{

		&cli.BoolFlag{
			Name:  "zstdchunked",
			Usage: "parse layer as zstd:chunked",
		},

		&cli.BoolFlag{
			Name:  "dump-toc",
			Usage: "dump TOC instead of digest. Note that the dumped TOC might be formatted with indents so may have different digest against the original in the layer",
		},
	},
	Action: func(ctx context.Context, clicontext *cli.Command) error {
		layerDgstStr := clicontext.Args().Get(0)
		if layerDgstStr == "" {
			return errors.New("layer digest need to be specified")
		}

		client, ctx, cancel, err := commands.NewClient(ctx, clicontext)
		if err != nil {
			return err
		}
		defer cancel()

		layerDgst, err := digest.Parse(layerDgstStr)
		if err != nil {
			return err
		}
		ra, err := client.ContentStore().ReaderAt(ctx, ocispec.Descriptor{Digest: layerDgst})
		if err != nil {
			return err
		}
		defer ra.Close()

		footerSize := estargz.FooterSize
		if clicontext.Bool("zstdchunked") {
			footerSize = zstdchunked.FooterSize
		}
		footer := make([]byte, footerSize)
		if _, err := ra.ReadAt(footer, ra.Size()-int64(footerSize)); err != nil {
			return fmt.Errorf("error reading footer: %w", err)
		}

		var decompressor estargz.Decompressor
		decompressor = new(estargz.GzipDecompressor)
		if clicontext.Bool("zstdchunked") {
			decompressor = new(zstdchunked.Decompressor)
		}

		_, tocOff, tocSize, err := decompressor.ParseFooter(footer)
		if err != nil {
			return fmt.Errorf("error parsing footer: %w", err)
		}
		if tocSize <= 0 {
			tocSize = ra.Size() - tocOff - int64(footerSize)
		}
		toc, tocDgst, err := decompressor.ParseTOC(io.NewSectionReader(ra, tocOff, tocSize))
		if err != nil {
			return fmt.Errorf("error parsing TOC: %w", err)
		}

		if clicontext.Bool("dump-toc") {
			tocJSON, err := json.MarshalIndent(toc, "", "\t")
			if err != nil {
				return fmt.Errorf("failed to marshal toc: %w", err)
			}
			fmt.Println(string(tocJSON))
			return nil
		}
		fmt.Println(tocDgst.String())
		return nil
	},
}

GetTOCDigestCommand outputs TOC info of a layer

View Source
var IPFSPushCommand = &cli.Command{
	Name:      "ipfs-push",
	Usage:     "push an image to IPFS (experimental)",
	ArgsUsage: "[flags] <image_ref>",
	Flags: []cli.Flag{

		&cli.StringSliceFlag{
			Name:  "platform",
			Usage: "Add content for a specific platform",
		},
		&cli.BoolFlag{
			Name:  "all-platforms",
			Usage: "Add content for all platforms",
		},
		&cli.BoolFlag{
			Name:  "estargz",
			Value: true,
			Usage: "Convert the image into eStargz",
		},
	},
	Action: func(ctx gocontext.Context, context *cli.Command) error {
		srcRef := context.Args().Get(0)
		if srcRef == "" {
			return errors.New("image need to be specified")
		}

		var platformMC platforms.MatchComparer
		if context.Bool("all-platforms") {
			platformMC = platforms.All
		} else {
			if pss := context.StringSlice("platform"); len(pss) > 0 {
				var all []ocispec.Platform
				for _, ps := range pss {
					p, err := platforms.Parse(ps)
					if err != nil {
						return fmt.Errorf("invalid platform %q: %w", ps, err)
					}
					all = append(all, p)
				}
				platformMC = platforms.Ordered(all...)
			} else {
				platformMC = platforms.DefaultStrict()
			}
		}

		client, ctx, cancel, err := commands.NewClient(ctx, context)
		if err != nil {
			return err
		}
		defer cancel()

		var layerConvert converter.ConvertFunc
		if context.Bool("estargz") {
			layerConvert = estargzconvert.LayerConvertFunc()
		}
		p, err := ipfs.Push(ctx, client, srcRef, layerConvert, platformMC)
		if err != nil {
			return err
		}
		log.L.WithField("CID", p).Infof("Pushed")
		fmt.Println(p)

		return nil
	},
}

IPFSPushCommand pushes an image to IPFS

View Source
var OptimizeCommand = &cli.Command{
	Name:      "optimize",
	Usage:     "optimize an image with user-specified workload",
	ArgsUsage: "[flags] <source_ref> <target_ref>...",
	Flags: append([]cli.Flag{
		&cli.BoolFlag{
			Name:  "reuse",
			Usage: "reuse eStargz (already optimized) layers without further conversion",
		},
		&cli.StringSliceFlag{
			Name:  "platform",
			Usage: "Pull content from a specific platform",
		},
		&cli.BoolFlag{
			Name:  "all-platforms",
			Usage: "targeting all platform of the source image",
		},
		&cli.BoolFlag{
			Name:  "wait-on-signal",
			Usage: "ignore context cancel and keep the container running until it receives SIGINT (Ctrl + C) sent manually",
		},
		&cli.StringFlag{
			Name:  "wait-on-line",
			Usage: "Substring of a stdout line to be waited. When this string is detected, the container will be killed.",
		},
		&cli.BoolFlag{
			Name:  "no-optimize",
			Usage: "convert image without optimization",
		},
		&cli.StringFlag{
			Name:  "record-out",
			Usage: "record the monitor log to the specified file",
		},
		&cli.BoolFlag{
			Name:  "oci",
			Usage: "convert Docker media types to OCI media types",
		},
		&cli.IntFlag{
			Name:  "estargz-compression-level",
			Usage: "eStargz compression level",
			Value: gzip.BestCompression,
		},
		&cli.BoolFlag{
			Name:  "estargz-external-toc",
			Usage: "Separate TOC JSON into another image (called \"TOC image\"). The name of TOC image is the original + \"-esgztoc\" suffix. Both eStargz and the TOC image should be pushed to the same registry. stargz-snapshotter refers to the TOC image when it pulls the result eStargz image.",
		},
		&cli.IntFlag{
			Name:  "estargz-chunk-size",
			Usage: "eStargz chunk size (not applied to zstd:chunked)",
			Value: 0,
		},
		&cli.IntFlag{
			Name:  "estargz-min-chunk-size",
			Usage: "The minimal number of bytes of data must be written in one gzip stream. Note that this adds a TOC property that old reader doesn't understand (not applied to zstd:chunked)",
			Value: 0,
		},
		&cli.IntFlag{
			Name:  "estargz-parallelism",
			Usage: "Number of workers used to build the layer. Pinning it makes builds reproducible across machines regardless of CPU count. 0 (default) uses GOMAXPROCS; 1 forces a sequential build. Has no effect with --estargz-min-chunk-size (not applied to zstd:chunked)",
			Value: 0,
		},
		&cli.StringFlag{
			Name:    "estargz-gzip-helper",
			Aliases: []string{"GH"},
			Usage:   "Helper command for decompressing layers compressed with gzip. Options: pigz, igzip, or gzip.",
			Value:   "",
		},
		&cli.BoolFlag{
			Name:  "zstdchunked",
			Usage: "use zstd compression instead of gzip (a.k.a zstd:chunked)",
		},
		&cli.IntFlag{
			Name:  "zstdchunked-compression-level",
			Usage: "zstd:chunked compression level",
			Value: 3,
		},
		&cli.StringFlag{
			Name:  "prefetch-list",
			Usage: "path to a text file listing files/patterns to prefetch (one per line; supports glob *, ?, [], and **)",
		},
	}, samplerFlags...),
	Action: func(ctx context.Context, clicontext *cli.Command) error {
		convertOpts := []converter.Opt{}
		srcRef := clicontext.Args().Get(0)
		targetRef := clicontext.Args().Get(1)
		if srcRef == "" || targetRef == "" {
			return errors.New("src and target image need to be specified")
		}

		if clicontext.Bool("no-optimize") && clicontext.String("prefetch-list") != "" {
			return errors.New("--no-optimize and --prefetch-list cannot be used together")
		}

		var platformMC platforms.MatchComparer
		if clicontext.Bool("all-platforms") {
			platformMC = platforms.All
		} else {
			if pss := clicontext.StringSlice("platform"); len(pss) > 0 {
				var all []ocispec.Platform
				for _, ps := range pss {
					p, err := platforms.Parse(ps)
					if err != nil {
						return fmt.Errorf("invalid platform %q: %w", ps, err)
					}
					all = append(all, p)
				}
				platformMC = platforms.Ordered(all...)
			} else {
				platformMC = platforms.DefaultStrict()
			}
		}
		convertOpts = append(convertOpts, converter.WithPlatform(platformMC))

		if clicontext.Bool("oci") {
			convertOpts = append(convertOpts, converter.WithDockerToOCI(true))
		} else if clicontext.Bool("zstdchunked") {
			return errors.New("option --zstdchunked must be used in conjunction with --oci")
		}

		client, ctx, cancel, err := commands.NewClient(ctx, clicontext)
		if err != nil {
			return err
		}
		defer cancel()

		ctx, done, err := client.WithLease(ctx)
		if err != nil {
			return err
		}
		defer done(ctx)

		recordOut, esgzOptsPerLayer, wrapper, err := analyze(ctx, clicontext, client, srcRef)
		if err != nil {
			return err
		}
		if recordOutFile := clicontext.String("record-out"); recordOutFile != "" {
			if err := writeContentFile(ctx, client, recordOut, recordOutFile); err != nil {
				return fmt.Errorf("failed output record file: %w", err)
			}
		}
		var f converter.ConvertFunc
		var finalize func(ctx context.Context, cs content.Store, ref string, desc *ocispec.Descriptor) (*images.Image, error)
		if clicontext.Bool("zstdchunked") {
			f = zstdchunkedconvert.LayerConvertWithLayerOptsFuncWithCompressionLevel(
				zstd.EncoderLevelFromZstd(clicontext.Int("zstdchunked-compression-level")), esgzOptsPerLayer)
		} else {
			esgzOpts := []estargz.Option{
				estargz.WithChunkSize(clicontext.Int("estargz-chunk-size")),
				estargz.WithMinChunkSize(clicontext.Int("estargz-min-chunk-size")),
				estargz.WithParallelism(clicontext.Int("estargz-parallelism")),
			}
			if estargzGzipHelper := clicontext.String("estargz-gzip-helper"); estargzGzipHelper != "" {
				gzipHelperFunc, err := decompressutil.GetGzipHelperFunc(estargzGzipHelper)
				if err != nil {
					return err
				}
				esgzOpts = append(esgzOpts, estargz.WithGzipHelperFunc(gzipHelperFunc))
			}
			if !clicontext.Bool("estargz-external-toc") {
				esgzOpts = append(esgzOpts, estargz.WithCompressionLevel(clicontext.Int("estargz-compression-level")))
				f = estargzconvert.LayerConvertWithLayerAndCommonOptsFunc(esgzOptsPerLayer, esgzOpts...)
			} else if clicontext.Bool("reuse") {

				return fmt.Errorf("\"estargz-external-toc\" can't be used with \"reuse\" flag")
			} else {
				f, finalize = esgzexternaltocconvert.LayerConvertWithLayerAndCommonOptsFunc(esgzOptsPerLayer, esgzOpts,
					clicontext.Int("estargz-compression-level"))
			}
		}
		if wrapper != nil {
			f = wrapper(f)
		}
		layerConvertFunc := logWrapper(f)

		sigCh := make(chan os.Signal, 1)
		signal.Notify(sigCh, os.Interrupt)
		go func() {

			select {
			case s := <-sigCh:
				log.G(ctx).Infof("Got %v", s)
				cancel()
			case <-ctx.Done():
			}
		}()
		convertOpts = append(convertOpts, converter.WithLayerConvertFunc(layerConvertFunc))
		newImg, err := converter.Convert(ctx, client, targetRef, srcRef, convertOpts...)
		if err != nil {
			return err
		}
		if finalize != nil {
			newI, err := finalize(ctx, client.ContentStore(), targetRef, &newImg.Target)
			if err != nil {
				return err
			}
			is := client.ImageService()
			_ = is.Delete(ctx, newI.Name)
			finimg, err := is.Create(ctx, *newI)
			if err != nil {
				return err
			}
			fmt.Fprintln(clicontext.Root().Writer, "extra image:", finimg.Name)
		}
		fmt.Fprintln(clicontext.Root().Writer, newImg.Target.Digest.String())
		return nil
	},
}

OptimizeCommand converts and optimizes an image

View Source
var RpullCommand = &cli.Command{
	Name:      "rpull",
	Usage:     "pull an image from a registry leveraging stargz snapshotter",
	ArgsUsage: "[flags] <ref>",
	Description: `Fetch and prepare an image for use in containerd leveraging stargz snapshotter.

After pulling an image, it should be ready to use the same reference in a run
command. 
`,
	Flags: append(append(commands.RegistryFlags, commands.LabelFlag,
		&cli.BoolFlag{
			Name:  skipContentVerifyOpt,
			Usage: "Skip content verification for layers contained in this image.",
		},
		&cli.BoolFlag{
			Name:  "ipfs",
			Usage: "Pull image from IPFS. Specify an IPFS CID as a reference. (experimental)",
		},
		&cli.BoolFlag{
			Name:  "use-containerd-labels",
			Usage: "Use labels defined in containerd project",
		},
	), commands.SnapshotterFlags...),
	Action: func(ctx context.Context, context *cli.Command) error {
		var (
			ref    = context.Args().First()
			config = &rPullConfig{}
		)
		if ref == "" {
			return fmt.Errorf("please provide an image reference to pull")
		}

		client, ctx, cancel, err := commands.NewClient(ctx, context)
		if err != nil {
			return err
		}
		defer cancel()

		ctx, done, err := client.WithLease(ctx)
		if err != nil {
			return err
		}
		defer done(ctx)

		fc, err := content.NewFetchConfig(ctx, context)
		if err != nil {
			return err
		}
		config.FetchConfig = fc
		config.containerdLabels = context.Bool("use-containerd-labels")

		if context.Bool(skipContentVerifyOpt) {
			config.skipVerify = true
		}

		if context.Bool("ipfs") {
			r, err := ipfs.NewResolver(ipfs.ResolverOptions{
				Scheme: "ipfs",
			})
			if err != nil {
				return err
			}
			config.Resolver = r
		}
		config.snapshotter = remoteSnapshotterName
		if sn := context.String("snapshotter"); sn != "" {
			config.snapshotter = sn
		}

		if err := verifier.VerifyImage(ctx, client, ref, config.Resolver); err != nil {
			return fmt.Errorf("verification error: %w", err)
		}

		return pull(ctx, client, ref, config)
	},
}

RpullCommand is a subcommand to pull an image from a registry leveraging stargz snapshotter

Functions

This section is empty.

Types

This section is empty.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL