412 lines
10 KiB
Go
412 lines
10 KiB
Go
package client
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"sync"
|
|
|
|
"github.com/hyperhq/hypercli/api/client/formatter"
|
|
Cli "github.com/hyperhq/hypercli/cli"
|
|
"github.com/hyperhq/hypercli/opts"
|
|
flag "github.com/hyperhq/hypercli/pkg/mflag"
|
|
|
|
"github.com/cheggaaa/pb"
|
|
"github.com/hyperhq/hyper-api/types"
|
|
"github.com/hyperhq/hyper-api/types/filters"
|
|
"golang.org/x/net/context"
|
|
)
|
|
|
|
// CmdVolume is the parent subcommand for all volume commands
|
|
//
|
|
// Usage: docker volume <COMMAND> <OPTS>
|
|
func (cli *DockerCli) CmdVolume(args ...string) error {
|
|
description := Cli.DockerCommands["volume"].Description + "\n\nCommands:\n"
|
|
commands := [][]string{
|
|
{"create", "Create a volume"},
|
|
{"inspect", "Return low-level information on a volume"},
|
|
{"ls", "List volumes"},
|
|
{"init", "Initialize volumes"},
|
|
{"rm", "Remove a volume"},
|
|
}
|
|
|
|
for _, cmd := range commands {
|
|
description += fmt.Sprintf(" %-25.25s%s\n", cmd[0], cmd[1])
|
|
}
|
|
|
|
description += "\nRun 'hyper volume COMMAND --help' for more information on a command"
|
|
cmd := Cli.Subcmd("volume", []string{"[COMMAND]"}, description, false)
|
|
|
|
cmd.Require(flag.Exact, 0)
|
|
err := cmd.ParseFlags(args, true)
|
|
cmd.Usage()
|
|
return err
|
|
}
|
|
|
|
// CmdVolumeLs outputs a list of Docker volumes.
|
|
//
|
|
// Usage: docker volume ls [OPTIONS]
|
|
func (cli *DockerCli) CmdVolumeLs(args ...string) error {
|
|
cmd := Cli.Subcmd("volume ls", nil, "List volumes", true)
|
|
|
|
quiet := cmd.Bool([]string{"q", "-quiet"}, false, "Only display volume names")
|
|
format := cmd.String([]string{"-format"}, "", "Pretty-print containers using a Go template")
|
|
flFilter := opts.NewListOpts(nil)
|
|
cmd.Var(&flFilter, []string{"f", "-filter"}, "Provide filter values (i.e. 'dangling=true')")
|
|
|
|
cmd.Require(flag.Exact, 0)
|
|
cmd.ParseFlags(args, true)
|
|
|
|
volFilterArgs := filters.NewArgs()
|
|
for _, f := range flFilter.GetAll() {
|
|
var err error
|
|
volFilterArgs, err = filters.ParseFlag(f, volFilterArgs)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
volumes, err := cli.client.VolumeList(context.Background(), volFilterArgs)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
/*
|
|
w := tabwriter.NewWriter(cli.out, 20, 1, 3, ' ', 0)
|
|
if !*quiet {
|
|
for _, warn := range volumes.Warnings {
|
|
fmt.Fprintln(cli.err, warn)
|
|
}
|
|
fmt.Fprintf(w, "DRIVER \tVOLUME NAME\tSIZE\tCONTAINER")
|
|
fmt.Fprintf(w, "\n")
|
|
}
|
|
|
|
for _, vol := range volumes.Volumes {
|
|
if *quiet {
|
|
fmt.Fprintln(w, vol.Name)
|
|
continue
|
|
}
|
|
var size, container string
|
|
if vol.Labels != nil {
|
|
size = vol.Labels["size"]
|
|
container = vol.Labels["container"]
|
|
if container != "" {
|
|
container = stringid.TruncateID(container)
|
|
}
|
|
}
|
|
fmt.Fprintf(w, "%s\t%s\t%s GB\t%s\n", vol.Driver, vol.Name, size, container)
|
|
}
|
|
w.Flush()
|
|
*/
|
|
f := *format
|
|
if len(f) == 0 {
|
|
if len(cli.VolumesFormat()) > 0 && !*quiet {
|
|
f = cli.VolumesFormat()
|
|
} else {
|
|
f = "table"
|
|
}
|
|
}
|
|
|
|
volCtx := formatter.VolumeContext{
|
|
Context: formatter.Context{
|
|
Output: cli.out,
|
|
Format: f,
|
|
Quiet: *quiet,
|
|
},
|
|
Volumes: volumes.Volumes,
|
|
}
|
|
|
|
volCtx.Write()
|
|
return nil
|
|
}
|
|
|
|
// CmdVolumeInspect displays low-level information on one or more volumes.
|
|
//
|
|
// Usage: docker volume inspect [OPTIONS] VOLUME [VOLUME...]
|
|
func (cli *DockerCli) CmdVolumeInspect(args ...string) error {
|
|
cmd := Cli.Subcmd("volume inspect", []string{"VOLUME [VOLUME...]"}, "Return low-level information on a volume", true)
|
|
tmplStr := cmd.String([]string{"f", "-format"}, "", "Format the output using the given go template")
|
|
|
|
cmd.Require(flag.Min, 1)
|
|
cmd.ParseFlags(args, true)
|
|
|
|
if err := cmd.Parse(args); err != nil {
|
|
return nil
|
|
}
|
|
|
|
ctx := context.Background()
|
|
|
|
inspectSearcher := func(name string) (interface{}, []byte, error) {
|
|
i, err := cli.client.VolumeInspect(ctx, name)
|
|
return i, nil, err
|
|
}
|
|
|
|
return cli.inspectElements(*tmplStr, cmd.Args(), inspectSearcher)
|
|
}
|
|
|
|
// CmdVolumeCreate creates a new volume.
|
|
//
|
|
// Usage: docker volume create [OPTIONS]
|
|
func (cli *DockerCli) CmdVolumeCreate(args ...string) error {
|
|
cmd := Cli.Subcmd("volume create", nil, "Create a volume", true)
|
|
flDriver := cmd.String([]string{}, "hyper", "Specify volume driver name")
|
|
flName := cmd.String([]string{"-name"}, "", "Specify volume name")
|
|
flSnapshot := cmd.String([]string{"-snapshot"}, "", "Specify snapshot to create volume")
|
|
flSize := cmd.Int([]string{"-size"}, 10, "Specify volume size")
|
|
|
|
cmd.Require(flag.Exact, 0)
|
|
cmd.ParseFlags(args, true)
|
|
|
|
volReq := types.VolumeCreateRequest{
|
|
Driver: *flDriver,
|
|
DriverOpts: make(map[string]string),
|
|
Name: *flName,
|
|
}
|
|
|
|
volReq.DriverOpts["size"] = fmt.Sprintf("%d", *flSize)
|
|
if *flSnapshot != "" {
|
|
volReq.DriverOpts["snapshot"] = *flSnapshot
|
|
if *flSize == 10 {
|
|
volReq.DriverOpts["size"] = ""
|
|
}
|
|
}
|
|
|
|
vol, err := cli.client.VolumeCreate(context.Background(), volReq)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
fmt.Fprintf(cli.out, "%s\n", vol.Name)
|
|
return nil
|
|
}
|
|
|
|
// CmdVolumeRm removes one or more volumes.
|
|
//
|
|
// Usage: docker volume rm VOLUME [VOLUME...]
|
|
func (cli *DockerCli) CmdVolumeRm(args ...string) error {
|
|
cmd := Cli.Subcmd("volume rm", []string{"VOLUME [VOLUME...]"}, "Remove a volume", true)
|
|
cmd.Require(flag.Min, 1)
|
|
cmd.ParseFlags(args, true)
|
|
|
|
var status = 0
|
|
ctx := context.Background()
|
|
for _, name := range cmd.Args() {
|
|
if err := cli.client.VolumeRemove(ctx, name); err != nil {
|
|
fmt.Fprintf(cli.err, "%s\n", err)
|
|
status = 1
|
|
continue
|
|
}
|
|
fmt.Fprintf(cli.out, "%s\n", name)
|
|
}
|
|
|
|
if status != 0 {
|
|
return Cli.StatusError{StatusCode: status}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func validateVolumeSource(source string) error {
|
|
switch {
|
|
case strings.HasPrefix(source, "git://"):
|
|
fallthrough
|
|
case strings.HasPrefix(source, "http://"):
|
|
fallthrough
|
|
case strings.HasPrefix(source, "https://"):
|
|
break
|
|
case filepath.VolumeName(source) != "":
|
|
fallthrough
|
|
case strings.HasPrefix(source, "/"):
|
|
info, err := os.Stat(source)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !info.Mode().IsDir() && !info.Mode().IsRegular() {
|
|
return fmt.Errorf("Unsupported local volume source(%s): %s", source, info.Mode().String())
|
|
}
|
|
break
|
|
default:
|
|
return fmt.Errorf("%s is not supported volume source", source)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func validateVolumeInitArgs(args []string, req *types.VolumesInitializeRequest) ([]int, error) {
|
|
var sourceType []int
|
|
for _, desc := range args {
|
|
idx := strings.LastIndexByte(desc, ':')
|
|
if idx == -1 || idx >= len(desc)-1 {
|
|
return nil, fmt.Errorf("%s does not match format SOURCE:VOLUME", desc)
|
|
}
|
|
source := desc[:idx]
|
|
name := desc[idx+1:]
|
|
if err := validateVolumeSource(source); err != nil {
|
|
return nil, err
|
|
}
|
|
pathType, source := convertToUnixPath(source)
|
|
req.Volume = append(req.Volume, types.VolumeInitDesc{
|
|
Name: name,
|
|
Source: source,
|
|
})
|
|
sourceType = append(sourceType, pathType)
|
|
}
|
|
return sourceType, nil
|
|
}
|
|
|
|
// CmdVolumeInit Initializes one or more volumes.
|
|
//
|
|
// Usage: docker volume init SOURCE:VOLUME [SOURCE:VOLUME...]
|
|
func (cli *DockerCli) CmdVolumeInit(args ...string) error {
|
|
cmd := Cli.Subcmd("volume init", []string{"SOURCE:VOLUME [SOURCE:VOLUME...]"}, "Initialize a volume", true)
|
|
cmd.Require(flag.Min, 1)
|
|
cmd.ParseFlags(args, true)
|
|
|
|
return cli.initVolumes(cmd.Args(), false)
|
|
}
|
|
|
|
func (cli *DockerCli) initVolumes(vols []string, reload bool) error {
|
|
var req types.VolumesInitializeRequest
|
|
pathType, err := validateVolumeInitArgs(vols, &req)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
ctx := context.Background()
|
|
req.Reload = reload
|
|
resp, err := cli.client.VolumeInitialize(ctx, req)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if len(resp.Session) == 0 {
|
|
return nil
|
|
}
|
|
// Upload local volumes
|
|
var wg sync.WaitGroup
|
|
var results []error
|
|
pool, err := pb.StartPool()
|
|
if err != nil {
|
|
// Ignore progress bar failures
|
|
fmt.Fprintf(cli.err, "Warning: do not show upload progress: %s\n", err.Error())
|
|
pool = nil
|
|
err = nil
|
|
}
|
|
for idx, desc := range req.Volume {
|
|
if url, ok := resp.Uploaders[desc.Name]; ok {
|
|
source := recoverPath(pathType[idx], desc.Source)
|
|
wg.Add(1)
|
|
go uploadLocalVolume(source, url, resp.Cookie, &results, &wg, pool)
|
|
}
|
|
}
|
|
|
|
wg.Wait()
|
|
if pool != nil {
|
|
pool.Stop()
|
|
}
|
|
for _, err = range results {
|
|
fmt.Fprintf(cli.err, "Upload local volume failed: %s\n", err.Error())
|
|
}
|
|
|
|
finishErr := cli.client.VolumeUploadFinish(ctx, resp.Session)
|
|
if err == nil {
|
|
err = finishErr
|
|
}
|
|
return err
|
|
}
|
|
|
|
func uploadLocalVolume(source, url, cookie string, results *[]error, wg *sync.WaitGroup, pool *pb.Pool) {
|
|
var (
|
|
resp io.ReadCloser
|
|
tar *TarFile
|
|
fullPath string
|
|
err error
|
|
)
|
|
|
|
defer func() {
|
|
if err != nil {
|
|
*results = append(*results, err)
|
|
}
|
|
wg.Done()
|
|
}()
|
|
|
|
fullPath, err = filepath.Abs(source)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
tar = NewTarFile(source, 512)
|
|
walkFunc := func(path string, info os.FileInfo, err error) error {
|
|
var relPath, linkName string
|
|
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if info.Mode()&os.ModeSymlink == os.ModeSymlink {
|
|
linkName, err = os.Readlink(path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if path == fullPath {
|
|
if info.IsDir() {
|
|
// "." as indicator that it is a dir volume
|
|
relPath = "."
|
|
} else {
|
|
relPath = filepath.Base(path)
|
|
}
|
|
} else {
|
|
relPath, err = filepath.Rel(fullPath, path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
tar.AddFile(info, relPath, linkName, path)
|
|
return nil
|
|
}
|
|
|
|
err = filepath.Walk(fullPath, walkFunc)
|
|
if err != nil {
|
|
return
|
|
}
|
|
if pool != nil {
|
|
tar.AllocBar(pool)
|
|
}
|
|
|
|
resp, err = sendTarball(url, cookie, tar)
|
|
if err != nil {
|
|
return
|
|
}
|
|
defer resp.Close()
|
|
}
|
|
|
|
func sendTarball(uri, cookie string, input io.ReadCloser) (io.ReadCloser, error) {
|
|
req, err := http.NewRequest("POST", uri+"?cookie="+cookie, input)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
req.Header.Set("Content-Type", "application/x-tar")
|
|
|
|
client := &http.Client{}
|
|
resp, err := client.Do(req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
defer resp.Body.Close()
|
|
buf := new(bytes.Buffer)
|
|
buf.ReadFrom(resp.Body)
|
|
if buf.Len() > 0 {
|
|
err = fmt.Errorf("%s: %s", http.StatusText(resp.StatusCode), buf.String())
|
|
} else {
|
|
err = fmt.Errorf("%s", http.StatusText(resp.StatusCode))
|
|
}
|
|
return nil, err
|
|
}
|
|
return resp.Body, nil
|
|
}
|