diff --git a/command/plugin/cmd.go b/command/plugin/cmd.go index 03d01c8882..e55f4ef327 100644 --- a/command/plugin/cmd.go +++ b/command/plugin/cmd.go @@ -28,6 +28,7 @@ func NewPluginCommand(dockerCli *command.DockerCli) *cobra.Command { newRemoveCommand(dockerCli), newSetCommand(dockerCli), newPushCommand(dockerCli), + newCreateCommand(dockerCli), ) return cmd } diff --git a/command/plugin/create.go b/command/plugin/create.go new file mode 100644 index 0000000000..3b18ed3750 --- /dev/null +++ b/command/plugin/create.go @@ -0,0 +1,125 @@ +package plugin + +import ( + "encoding/json" + "fmt" + "io" + "os" + "path/filepath" + + "github.com/Sirupsen/logrus" + "github.com/docker/docker/api/types" + "github.com/docker/docker/cli" + "github.com/docker/docker/cli/command" + "github.com/docker/docker/pkg/archive" + "github.com/docker/docker/reference" + "github.com/spf13/cobra" + "golang.org/x/net/context" +) + +// validateTag checks if the given repoName can be resolved. +func validateTag(rawRepo string) error { + _, err := reference.ParseNamed(rawRepo) + + return err +} + +// validateManifest ensures that a valid manifest.json is available in the given path +func validateManifest(path string) error { + dt, err := os.Open(filepath.Join(path, "manifest.json")) + if err != nil { + return err + } + + m := types.PluginManifest{} + err = json.NewDecoder(dt).Decode(&m) + dt.Close() + + return err +} + +// validateContextDir validates the given dir and returns abs path on success. +func validateContextDir(contextDir string) (string, error) { + absContextDir, err := filepath.Abs(contextDir) + + stat, err := os.Lstat(absContextDir) + if err != nil { + return "", err + } + + if !stat.IsDir() { + return "", fmt.Errorf("context must be a directory") + } + + return absContextDir, nil +} + +type pluginCreateOptions struct { + repoName string + context string + compress bool +} + +func newCreateCommand(dockerCli *command.DockerCli) *cobra.Command { + options := pluginCreateOptions{} + + cmd := &cobra.Command{ + Use: "create [OPTIONS] reponame[:tag] PATH-TO-ROOTFS (rootfs + manifest.json)", + Short: "Create a plugin from a rootfs and manifest", + Args: cli.RequiresMinArgs(2), + RunE: func(cmd *cobra.Command, args []string) error { + options.repoName = args[0] + options.context = args[1] + return runCreate(dockerCli, options) + }, + } + + flags := cmd.Flags() + + flags.BoolVar(&options.compress, "compress", false, "Compress the context using gzip") + + return cmd +} + +func runCreate(dockerCli *command.DockerCli, options pluginCreateOptions) error { + var ( + createCtx io.ReadCloser + err error + ) + + if err := validateTag(options.repoName); err != nil { + return err + } + + absContextDir, err := validateContextDir(options.context) + if err != nil { + return err + } + + if err := validateManifest(options.context); err != nil { + return err + } + + compression := archive.Uncompressed + if options.compress { + logrus.Debugf("compression enabled") + compression = archive.Gzip + } + + createCtx, err = archive.TarWithOptions(absContextDir, &archive.TarOptions{ + Compression: compression, + }) + + if err != nil { + return err + } + + ctx := context.Background() + + createOptions := types.PluginCreateOptions{RepoName: options.repoName} + if err = dockerCli.Client().PluginCreate(ctx, createCtx, createOptions); err != nil { + return err + } + fmt.Fprintln(dockerCli.Out(), options.repoName) + return nil +}