github.com/khulnasoft/cli@v0.0.0-20240402070845-01bcad7beefa/cli/command/plugin/create.go (about)

     1  package plugin
     2  
     3  import (
     4  	"context"
     5  	"encoding/json"
     6  	"fmt"
     7  	"io"
     8  	"os"
     9  	"path/filepath"
    10  
    11  	"github.com/distribution/reference"
    12  	"github.com/docker/docker/api/types"
    13  	"github.com/docker/docker/pkg/archive"
    14  	"github.com/khulnasoft/cli/cli"
    15  	"github.com/khulnasoft/cli/cli/command"
    16  	"github.com/khulnasoft/cli/cli/command/completion"
    17  	"github.com/pkg/errors"
    18  	"github.com/sirupsen/logrus"
    19  	"github.com/spf13/cobra"
    20  )
    21  
    22  // validateTag checks if the given repoName can be resolved.
    23  func validateTag(rawRepo string) error {
    24  	_, err := reference.ParseNormalizedNamed(rawRepo)
    25  
    26  	return err
    27  }
    28  
    29  // validateConfig ensures that a valid config.json is available in the given path
    30  func validateConfig(path string) error {
    31  	dt, err := os.Open(filepath.Join(path, "config.json"))
    32  	if err != nil {
    33  		return err
    34  	}
    35  
    36  	m := types.PluginConfig{}
    37  	err = json.NewDecoder(dt).Decode(&m)
    38  	dt.Close()
    39  
    40  	return err
    41  }
    42  
    43  // validateContextDir validates the given dir and returns abs path on success.
    44  func validateContextDir(contextDir string) (string, error) {
    45  	absContextDir, err := filepath.Abs(contextDir)
    46  	if err != nil {
    47  		return "", err
    48  	}
    49  	stat, err := os.Lstat(absContextDir)
    50  	if err != nil {
    51  		return "", err
    52  	}
    53  
    54  	if !stat.IsDir() {
    55  		return "", errors.Errorf("context must be a directory")
    56  	}
    57  
    58  	return absContextDir, nil
    59  }
    60  
    61  type pluginCreateOptions struct {
    62  	repoName string
    63  	context  string
    64  	compress bool
    65  }
    66  
    67  func newCreateCommand(dockerCli command.Cli) *cobra.Command {
    68  	options := pluginCreateOptions{}
    69  
    70  	cmd := &cobra.Command{
    71  		Use:   "create [OPTIONS] PLUGIN PLUGIN-DATA-DIR",
    72  		Short: "Create a plugin from a rootfs and configuration. Plugin data directory must contain config.json and rootfs directory.",
    73  		Args:  cli.RequiresMinArgs(2),
    74  		RunE: func(cmd *cobra.Command, args []string) error {
    75  			options.repoName = args[0]
    76  			options.context = args[1]
    77  			return runCreate(cmd.Context(), dockerCli, options)
    78  		},
    79  		ValidArgsFunction: completion.NoComplete,
    80  	}
    81  
    82  	flags := cmd.Flags()
    83  
    84  	flags.BoolVar(&options.compress, "compress", false, "Compress the context using gzip")
    85  
    86  	return cmd
    87  }
    88  
    89  func runCreate(ctx context.Context, dockerCli command.Cli, options pluginCreateOptions) error {
    90  	var (
    91  		createCtx io.ReadCloser
    92  		err       error
    93  	)
    94  
    95  	if err := validateTag(options.repoName); err != nil {
    96  		return err
    97  	}
    98  
    99  	absContextDir, err := validateContextDir(options.context)
   100  	if err != nil {
   101  		return err
   102  	}
   103  
   104  	if err := validateConfig(options.context); err != nil {
   105  		return err
   106  	}
   107  
   108  	compression := archive.Uncompressed
   109  	if options.compress {
   110  		logrus.Debugf("compression enabled")
   111  		compression = archive.Gzip
   112  	}
   113  
   114  	createCtx, err = archive.TarWithOptions(absContextDir, &archive.TarOptions{
   115  		Compression: compression,
   116  	})
   117  	if err != nil {
   118  		return err
   119  	}
   120  
   121  	createOptions := types.PluginCreateOptions{RepoName: options.repoName}
   122  	if err = dockerCli.Client().PluginCreate(ctx, createCtx, createOptions); err != nil {
   123  		return err
   124  	}
   125  	fmt.Fprintln(dockerCli.Out(), options.repoName)
   126  	return nil
   127  }