aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBrent Baude <bbaude@redhat.com>2020-03-20 12:37:37 -0500
committerBrent Baude <bbaude@redhat.com>2020-03-20 18:27:08 -0500
commitc3a9ff11743c1ee25515ced348ec635fb7703aa0 (patch)
tree380dedfa9ffb337ebe197b15c218b3a1bfc627b6
parent7a095af92a6a39845b1c362222b23632f3e17001 (diff)
downloadpodman-c3a9ff11743c1ee25515ced348ec635fb7703aa0.tar.gz
podman-c3a9ff11743c1ee25515ced348ec635fb7703aa0.tar.bz2
podman-c3a9ff11743c1ee25515ced348ec635fb7703aa0.zip
podmanv2 volume create
add volume create Signed-off-by: Brent Baude <bbaude@redhat.com>
-rw-r--r--cmd/podmanV2/parse/parse.go188
-rw-r--r--cmd/podmanV2/parse/parse_test.go152
-rw-r--r--cmd/podmanV2/volumes/create.go72
-rw-r--r--cmd/podmanV2/volumes/volume.go10
-rw-r--r--pkg/api/handlers/libpod/volumes.go20
-rw-r--r--pkg/api/handlers/types.go12
-rw-r--r--pkg/api/server/swagger.go3
-rw-r--r--pkg/bindings/test/volumes_test.go29
-rw-r--r--pkg/bindings/volumes/volumes.go6
-rw-r--r--pkg/domain/entities/engine_container.go1
-rw-r--r--pkg/domain/entities/volumes.go41
-rw-r--r--pkg/domain/infra/abi/parse/parse.go68
-rw-r--r--pkg/domain/infra/abi/volumes.go38
-rw-r--r--pkg/domain/infra/tunnel/volumes.go16
14 files changed, 615 insertions, 41 deletions
diff --git a/cmd/podmanV2/parse/parse.go b/cmd/podmanV2/parse/parse.go
new file mode 100644
index 000000000..03cda268c
--- /dev/null
+++ b/cmd/podmanV2/parse/parse.go
@@ -0,0 +1,188 @@
+//nolint
+// most of these validate and parse functions have been taken from projectatomic/docker
+// and modified for cri-o
+package parse
+
+import (
+ "bufio"
+ "fmt"
+ "net"
+ "net/url"
+ "os"
+ "regexp"
+ "strings"
+
+ "github.com/pkg/errors"
+)
+
+const (
+ Protocol_TCP Protocol = 0
+ Protocol_UDP Protocol = 1
+)
+
+type Protocol int32
+
+// PortMapping specifies the port mapping configurations of a sandbox.
+type PortMapping struct {
+ // Protocol of the port mapping.
+ Protocol Protocol `protobuf:"varint,1,opt,name=protocol,proto3,enum=runtime.Protocol" json:"protocol,omitempty"`
+ // Port number within the container. Default: 0 (not specified).
+ ContainerPort int32 `protobuf:"varint,2,opt,name=container_port,json=containerPort,proto3" json:"container_port,omitempty"`
+ // Port number on the host. Default: 0 (not specified).
+ HostPort int32 `protobuf:"varint,3,opt,name=host_port,json=hostPort,proto3" json:"host_port,omitempty"`
+ // Host IP.
+ HostIp string `protobuf:"bytes,4,opt,name=host_ip,json=hostIp,proto3" json:"host_ip,omitempty"`
+}
+
+// Note: for flags that are in the form <number><unit>, use the RAMInBytes function
+// from the units package in docker/go-units/size.go
+
+var (
+ whiteSpaces = " \t"
+ alphaRegexp = regexp.MustCompile(`[a-zA-Z]`)
+ domainRegexp = regexp.MustCompile(`^(:?(:?[a-zA-Z0-9]|(:?[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9]))(:?\.(:?[a-zA-Z0-9]|(:?[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])))*)\.?\s*$`)
+)
+
+// validateExtraHost validates that the specified string is a valid extrahost and returns it.
+// ExtraHost is in the form of name:ip where the ip has to be a valid ip (ipv4 or ipv6).
+// for add-host flag
+func ValidateExtraHost(val string) (string, error) { //nolint
+ // allow for IPv6 addresses in extra hosts by only splitting on first ":"
+ arr := strings.SplitN(val, ":", 2)
+ if len(arr) != 2 || len(arr[0]) == 0 {
+ return "", fmt.Errorf("bad format for add-host: %q", val)
+ }
+ if _, err := validateIPAddress(arr[1]); err != nil {
+ return "", fmt.Errorf("invalid IP address in add-host: %q", arr[1])
+ }
+ return val, nil
+}
+
+// validateIPAddress validates an Ip address.
+// for dns, ip, and ip6 flags also
+func validateIPAddress(val string) (string, error) {
+ var ip = net.ParseIP(strings.TrimSpace(val))
+ if ip != nil {
+ return ip.String(), nil
+ }
+ return "", fmt.Errorf("%s is not an ip address", val)
+}
+
+func ValidateDomain(val string) (string, error) {
+ if alphaRegexp.FindString(val) == "" {
+ return "", fmt.Errorf("%s is not a valid domain", val)
+ }
+ ns := domainRegexp.FindSubmatch([]byte(val))
+ if len(ns) > 0 && len(ns[1]) < 255 {
+ return string(ns[1]), nil
+ }
+ return "", fmt.Errorf("%s is not a valid domain", val)
+}
+
+// GetAllLabels retrieves all labels given a potential label file and a number
+// of labels provided from the command line.
+func GetAllLabels(labelFile, inputLabels []string) (map[string]string, error) {
+ labels := make(map[string]string)
+ for _, file := range labelFile {
+ // Use of parseEnvFile still seems safe, as it's missing the
+ // extra parsing logic of parseEnv.
+ // There's an argument that we SHOULD be doing that parsing for
+ // all environment variables, even those sourced from files, but
+ // that would require a substantial rework.
+ if err := parseEnvFile(labels, file); err != nil {
+ // FIXME: parseEnvFile is using parseEnv, so we need to add extra
+ // logic for labels.
+ return nil, err
+ }
+ }
+ for _, label := range inputLabels {
+ split := strings.SplitN(label, "=", 2)
+ if split[0] == "" {
+ return nil, errors.Errorf("invalid label format: %q", label)
+ }
+ value := ""
+ if len(split) > 1 {
+ value = split[1]
+ }
+ labels[split[0]] = value
+ }
+ return labels, nil
+}
+
+func parseEnv(env map[string]string, line string) error {
+ data := strings.SplitN(line, "=", 2)
+
+ // catch invalid variables such as "=" or "=A"
+ if data[0] == "" {
+ return errors.Errorf("invalid environment variable: %q", line)
+ }
+
+ // trim the front of a variable, but nothing else
+ name := strings.TrimLeft(data[0], whiteSpaces)
+ if strings.ContainsAny(name, whiteSpaces) {
+ return errors.Errorf("name %q has white spaces, poorly formatted name", name)
+ }
+
+ if len(data) > 1 {
+ env[name] = data[1]
+ } else {
+ if strings.HasSuffix(name, "*") {
+ name = strings.TrimSuffix(name, "*")
+ for _, e := range os.Environ() {
+ part := strings.SplitN(e, "=", 2)
+ if len(part) < 2 {
+ continue
+ }
+ if strings.HasPrefix(part[0], name) {
+ env[part[0]] = part[1]
+ }
+ }
+ } else {
+ // if only a pass-through variable is given, clean it up.
+ if val, ok := os.LookupEnv(name); ok {
+ env[name] = val
+ }
+ }
+ }
+ return nil
+}
+
+// parseEnvFile reads a file with environment variables enumerated by lines
+func parseEnvFile(env map[string]string, filename string) error {
+ fh, err := os.Open(filename)
+ if err != nil {
+ return err
+ }
+ defer fh.Close()
+
+ scanner := bufio.NewScanner(fh)
+ for scanner.Scan() {
+ // trim the line from all leading whitespace first
+ line := strings.TrimLeft(scanner.Text(), whiteSpaces)
+ // line is not empty, and not starting with '#'
+ if len(line) > 0 && !strings.HasPrefix(line, "#") {
+ if err := parseEnv(env, line); err != nil {
+ return err
+ }
+ }
+ }
+ return scanner.Err()
+}
+
+// ValidateFileName returns an error if filename contains ":"
+// as it is currently not supported
+func ValidateFileName(filename string) error {
+ if strings.Contains(filename, ":") {
+ return errors.Errorf("invalid filename (should not contain ':') %q", filename)
+ }
+ return nil
+}
+
+// ValidURL checks a string urlStr is a url or not
+func ValidURL(urlStr string) error {
+ _, err := url.ParseRequestURI(urlStr)
+ if err != nil {
+ return errors.Wrapf(err, "invalid url path: %q", urlStr)
+ }
+ return nil
+}
diff --git a/cmd/podmanV2/parse/parse_test.go b/cmd/podmanV2/parse/parse_test.go
new file mode 100644
index 000000000..a6ddc2be9
--- /dev/null
+++ b/cmd/podmanV2/parse/parse_test.go
@@ -0,0 +1,152 @@
+//nolint
+// most of these validate and parse functions have been taken from projectatomic/docker
+// and modified for cri-o
+package parse
+
+import (
+ "io/ioutil"
+ "os"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+)
+
+var (
+ Var1 = []string{"ONE=1", "TWO=2"}
+)
+
+func createTmpFile(content []byte) (string, error) {
+ tmpfile, err := ioutil.TempFile(os.TempDir(), "unittest")
+ if err != nil {
+ return "", err
+ }
+
+ if _, err := tmpfile.Write(content); err != nil {
+ return "", err
+
+ }
+ if err := tmpfile.Close(); err != nil {
+ return "", err
+ }
+ return tmpfile.Name(), nil
+}
+
+func TestValidateExtraHost(t *testing.T) {
+ type args struct {
+ val string
+ }
+ tests := []struct {
+ name string
+ args args
+ want string
+ wantErr bool
+ }{
+ //2001:0db8:85a3:0000:0000:8a2e:0370:7334
+ {name: "good-ipv4", args: args{val: "foobar:192.168.1.1"}, want: "foobar:192.168.1.1", wantErr: false},
+ {name: "bad-ipv4", args: args{val: "foobar:999.999.999.99"}, want: "", wantErr: true},
+ {name: "bad-ipv4", args: args{val: "foobar:999.999.999"}, want: "", wantErr: true},
+ {name: "noname-ipv4", args: args{val: "192.168.1.1"}, want: "", wantErr: true},
+ {name: "noname-ipv4", args: args{val: ":192.168.1.1"}, want: "", wantErr: true},
+ {name: "noip", args: args{val: "foobar:"}, want: "", wantErr: true},
+ {name: "noip", args: args{val: "foobar"}, want: "", wantErr: true},
+ {name: "good-ipv6", args: args{val: "foobar:2001:0db8:85a3:0000:0000:8a2e:0370:7334"}, want: "foobar:2001:0db8:85a3:0000:0000:8a2e:0370:7334", wantErr: false},
+ {name: "bad-ipv6", args: args{val: "foobar:0db8:85a3:0000:0000:8a2e:0370:7334"}, want: "", wantErr: true},
+ {name: "bad-ipv6", args: args{val: "foobar:0db8:85a3:0000:0000:8a2e:0370:7334.0000.0000.000"}, want: "", wantErr: true},
+ {name: "noname-ipv6", args: args{val: "2001:0db8:85a3:0000:0000:8a2e:0370:7334"}, want: "", wantErr: true},
+ {name: "noname-ipv6", args: args{val: ":2001:0db8:85a3:0000:0000:8a2e:0370:7334"}, want: "", wantErr: true},
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ got, err := ValidateExtraHost(tt.args.val)
+ if (err != nil) != tt.wantErr {
+ t.Errorf("ValidateExtraHost() error = %v, wantErr %v", err, tt.wantErr)
+ return
+ }
+ if got != tt.want {
+ t.Errorf("ValidateExtraHost() = %v, want %v", got, tt.want)
+ }
+ })
+ }
+}
+
+func Test_validateIPAddress(t *testing.T) {
+ type args struct {
+ val string
+ }
+ tests := []struct {
+ name string
+ args args
+ want string
+ wantErr bool
+ }{
+ {name: "ipv4-good", args: args{val: "192.168.1.1"}, want: "192.168.1.1", wantErr: false},
+ {name: "ipv4-bad", args: args{val: "192.168.1.1.1"}, want: "", wantErr: true},
+ {name: "ipv4-bad", args: args{val: "192."}, want: "", wantErr: true},
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ got, err := validateIPAddress(tt.args.val)
+ if (err != nil) != tt.wantErr {
+ t.Errorf("validateIPAddress() error = %v, wantErr %v", err, tt.wantErr)
+ return
+ }
+ if got != tt.want {
+ t.Errorf("validateIPAddress() = %v, want %v", got, tt.want)
+ }
+ })
+ }
+}
+
+func TestValidateFileName(t *testing.T) {
+ type args struct {
+ filename string
+ }
+ tests := []struct {
+ name string
+ args args
+ wantErr bool
+ }{
+ {name: "good", args: args{filename: "/some/rand/path"}, wantErr: false},
+ {name: "good", args: args{filename: "some/rand/path"}, wantErr: false},
+ {name: "good", args: args{filename: "/"}, wantErr: false},
+ {name: "bad", args: args{filename: "/:"}, wantErr: true},
+ {name: "bad", args: args{filename: ":/"}, wantErr: true},
+ {name: "bad", args: args{filename: "/some/rand:/path"}, wantErr: true},
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ if err := ValidateFileName(tt.args.filename); (err != nil) != tt.wantErr {
+ t.Errorf("ValidateFileName() error = %v, wantErr %v", err, tt.wantErr)
+ }
+ })
+ }
+}
+
+func TestGetAllLabels(t *testing.T) {
+ fileLabels := []string{}
+ labels, _ := GetAllLabels(fileLabels, Var1)
+ assert.Equal(t, len(labels), 2)
+}
+
+func TestGetAllLabelsBadKeyValue(t *testing.T) {
+ inLabels := []string{"=badValue", "="}
+ fileLabels := []string{}
+ _, err := GetAllLabels(fileLabels, inLabels)
+ assert.Error(t, err, assert.AnError)
+}
+
+func TestGetAllLabelsBadLabelFile(t *testing.T) {
+ fileLabels := []string{"/foobar5001/be"}
+ _, err := GetAllLabels(fileLabels, Var1)
+ assert.Error(t, err, assert.AnError)
+}
+
+func TestGetAllLabelsFile(t *testing.T) {
+ content := []byte("THREE=3")
+ tFile, err := createTmpFile(content)
+ defer os.Remove(tFile)
+ assert.NoError(t, err)
+ fileLabels := []string{tFile}
+ result, _ := GetAllLabels(fileLabels, Var1)
+ assert.Equal(t, len(result), 3)
+}
diff --git a/cmd/podmanV2/volumes/create.go b/cmd/podmanV2/volumes/create.go
new file mode 100644
index 000000000..91181dd03
--- /dev/null
+++ b/cmd/podmanV2/volumes/create.go
@@ -0,0 +1,72 @@
+package volumes
+
+import (
+ "context"
+ "fmt"
+
+ "github.com/containers/libpod/cmd/podmanV2/parse"
+ "github.com/containers/libpod/cmd/podmanV2/registry"
+ "github.com/containers/libpod/pkg/domain/entities"
+ "github.com/pkg/errors"
+ "github.com/spf13/cobra"
+)
+
+var (
+ createDescription = `If using the default driver, "local", the volume will be created on the host in the volumes directory under container storage.`
+
+ createCommand = &cobra.Command{
+ Use: "create [flags] [NAME]",
+ Short: "Create a new volume",
+ Long: createDescription,
+ RunE: create,
+ Example: `podman volume create myvol
+ podman volume create
+ podman volume create --label foo=bar myvol`,
+ }
+)
+
+var (
+ createOpts = entities.VolumeCreateOptions{}
+ opts = struct {
+ Label []string
+ Opts []string
+ }{}
+)
+
+func init() {
+ registry.Commands = append(registry.Commands, registry.CliCommand{
+ Mode: []entities.EngineMode{entities.ABIMode, entities.TunnelMode},
+ Command: createCommand,
+ Parent: volumeCmd,
+ })
+ flags := createCommand.Flags()
+ flags.StringVar(&createOpts.Driver, "driver", "", "Specify volume driver name (default local)")
+ flags.StringSliceVarP(&opts.Label, "label", "l", []string{}, "Set metadata for a volume (default [])")
+ flags.StringArrayVarP(&opts.Opts, "opt", "o", []string{}, "Set driver specific options (default [])")
+}
+
+func create(cmd *cobra.Command, args []string) error {
+ var (
+ err error
+ )
+ if len(args) > 1 {
+ return errors.Errorf("too many arguments, create takes at most 1 argument")
+ }
+ if len(args) > 0 {
+ createOpts.Name = args[0]
+ }
+ createOpts.Label, err = parse.GetAllLabels([]string{}, opts.Label)
+ if err != nil {
+ return errors.Wrapf(err, "unable to process labels")
+ }
+ createOpts.Options, err = parse.GetAllLabels([]string{}, opts.Opts)
+ if err != nil {
+ return errors.Wrapf(err, "unable to process options")
+ }
+ response, err := registry.ContainerEngine().VolumeCreate(context.Background(), createOpts)
+ if err != nil {
+ return err
+ }
+ fmt.Println(response.IdOrName)
+ return nil
+}
diff --git a/cmd/podmanV2/volumes/volume.go b/cmd/podmanV2/volumes/volume.go
index 245c06da0..84abe3d24 100644
--- a/cmd/podmanV2/volumes/volume.go
+++ b/cmd/podmanV2/volumes/volume.go
@@ -1,4 +1,4 @@
-package images
+package volumes
import (
"github.com/containers/libpod/cmd/podmanV2/registry"
@@ -8,7 +8,7 @@ import (
var (
// Command: podman _volume_
- cmd = &cobra.Command{
+ volumeCmd = &cobra.Command{
Use: "volume",
Short: "Manage volumes",
Long: "Volumes are created in and can be shared between containers",
@@ -21,10 +21,10 @@ var (
func init() {
registry.Commands = append(registry.Commands, registry.CliCommand{
Mode: []entities.EngineMode{entities.ABIMode, entities.TunnelMode},
- Command: cmd,
+ Command: volumeCmd,
})
- cmd.SetHelpTemplate(registry.HelpTemplate())
- cmd.SetUsageTemplate(registry.UsageTemplate())
+ volumeCmd.SetHelpTemplate(registry.HelpTemplate())
+ volumeCmd.SetUsageTemplate(registry.UsageTemplate())
}
func preRunE(cmd *cobra.Command, args []string) error {
diff --git a/pkg/api/handlers/libpod/volumes.go b/pkg/api/handlers/libpod/volumes.go
index 9b10ee890..06ca1d225 100644
--- a/pkg/api/handlers/libpod/volumes.go
+++ b/pkg/api/handlers/libpod/volumes.go
@@ -8,8 +8,8 @@ import (
"github.com/containers/libpod/cmd/podman/shared"
"github.com/containers/libpod/libpod"
"github.com/containers/libpod/libpod/define"
- "github.com/containers/libpod/pkg/api/handlers"
"github.com/containers/libpod/pkg/api/handlers/utils"
+ "github.com/containers/libpod/pkg/domain/entities"
"github.com/gorilla/schema"
"github.com/pkg/errors"
log "github.com/sirupsen/logrus"
@@ -25,7 +25,7 @@ func CreateVolume(w http.ResponseWriter, r *http.Request) {
}{
// override any golang type defaults
}
- input := handlers.VolumeCreateConfig{}
+ input := entities.VolumeCreateOptions{}
if err := decoder.Decode(&query, r.URL.Query()); err != nil {
utils.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest,
errors.Wrapf(err, "Failed to parse parameters for %s", r.URL.String()))
@@ -46,8 +46,8 @@ func CreateVolume(w http.ResponseWriter, r *http.Request) {
if len(input.Label) > 0 {
volumeOptions = append(volumeOptions, libpod.WithVolumeLabels(input.Label))
}
- if len(input.Opts) > 0 {
- parsedOptions, err := shared.ParseVolumeOptions(input.Opts)
+ if len(input.Options) > 0 {
+ parsedOptions, err := shared.ParseVolumeOptions(input.Options)
if err != nil {
utils.InternalServerError(w, err)
return
@@ -64,7 +64,17 @@ func CreateVolume(w http.ResponseWriter, r *http.Request) {
utils.InternalServerError(w, err)
return
}
- utils.WriteResponse(w, http.StatusOK, config)
+ volResponse := entities.VolumeConfigResponse{
+ Name: config.Name,
+ Labels: config.Labels,
+ Driver: config.Driver,
+ MountPoint: config.MountPoint,
+ CreatedTime: config.CreatedTime,
+ Options: config.Options,
+ UID: config.UID,
+ GID: config.GID,
+ }
+ utils.WriteResponse(w, http.StatusOK, volResponse)
}
func InspectVolume(w http.ResponseWriter, r *http.Request) {
diff --git a/pkg/api/handlers/types.go b/pkg/api/handlers/types.go
index ce4a9957b..c6b70251b 100644
--- a/pkg/api/handlers/types.go
+++ b/pkg/api/handlers/types.go
@@ -128,18 +128,6 @@ type CreateContainerConfig struct {
NetworkingConfig dockerNetwork.NetworkingConfig
}
-// swagger:model VolumeCreate
-type VolumeCreateConfig struct {
- // New volume's name. Can be left blank
- Name string `schema:"name"`
- // Volume driver to use
- Driver string `schema:"driver"`
- // User-defined key/value metadata.
- Label map[string]string `schema:"label"`
- // Mapping of driver options and values.
- Opts map[string]string `schema:"opts"`
-}
-
// swagger:model IDResponse
type IDResponse struct {
// ID
diff --git a/pkg/api/server/swagger.go b/pkg/api/server/swagger.go
index d2cf7503e..2e1a269f2 100644
--- a/pkg/api/server/swagger.go
+++ b/pkg/api/server/swagger.go
@@ -4,6 +4,7 @@ import (
"github.com/containers/libpod/libpod"
"github.com/containers/libpod/pkg/api/handlers"
"github.com/containers/libpod/pkg/api/handlers/utils"
+ "github.com/containers/libpod/pkg/domain/entities"
)
// No such image
@@ -155,7 +156,7 @@ type ok struct {
type swagVolumeCreateResponse struct {
// in:body
Body struct {
- libpod.VolumeConfig
+ entities.VolumeConfigResponse
}
}
diff --git a/pkg/bindings/test/volumes_test.go b/pkg/bindings/test/volumes_test.go
index 1d5ae1329..9da034d24 100644
--- a/pkg/bindings/test/volumes_test.go
+++ b/pkg/bindings/test/volumes_test.go
@@ -6,11 +6,10 @@ import (
"net/http"
"time"
- "github.com/containers/libpod/pkg/api/handlers"
+ "github.com/containers/libpod/pkg/bindings"
"github.com/containers/libpod/pkg/bindings/containers"
"github.com/containers/libpod/pkg/bindings/volumes"
-
- "github.com/containers/libpod/pkg/bindings"
+ "github.com/containers/libpod/pkg/domain/entities"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
"github.com/onsi/gomega/gexec"
@@ -53,13 +52,13 @@ var _ = Describe("Podman volumes", func() {
It("create volume", func() {
// create a volume with blank config should work
- _, err := volumes.Create(connText, handlers.VolumeCreateConfig{})
+ _, err := volumes.Create(connText, entities.VolumeCreateOptions{})
Expect(err).To(BeNil())
- vcc := handlers.VolumeCreateConfig{
- Name: "foobar",
- Label: nil,
- Opts: nil,
+ vcc := entities.VolumeCreateOptions{
+ Name: "foobar",
+ Label: nil,
+ Options: nil,
}
vol, err := volumes.Create(connText, vcc)
Expect(err).To(BeNil())
@@ -73,7 +72,7 @@ var _ = Describe("Podman volumes", func() {
})
It("inspect volume", func() {
- vol, err := volumes.Create(connText, handlers.VolumeCreateConfig{})
+ vol, err := volumes.Create(connText, entities.VolumeCreateOptions{})
Expect(err).To(BeNil())
data, err := volumes.Inspect(connText, vol.Name)
Expect(err).To(BeNil())
@@ -87,13 +86,13 @@ var _ = Describe("Podman volumes", func() {
Expect(code).To(BeNumerically("==", http.StatusNotFound))
// Removing an unused volume should work
- vol, err := volumes.Create(connText, handlers.VolumeCreateConfig{})
+ vol, err := volumes.Create(connText, entities.VolumeCreateOptions{})
Expect(err).To(BeNil())
err = volumes.Remove(connText, vol.Name, nil)
Expect(err).To(BeNil())
// Removing a volume that is being used without force should be 409
- vol, err = volumes.Create(connText, handlers.VolumeCreateConfig{})
+ vol, err = volumes.Create(connText, entities.VolumeCreateOptions{})
Expect(err).To(BeNil())
session := bt.runPodman([]string{"run", "-dt", "-v", fmt.Sprintf("%s:/foobar", vol.Name), "--name", "vtest", alpine.name, "top"})
session.Wait(45)
@@ -119,7 +118,7 @@ var _ = Describe("Podman volumes", func() {
// create a bunch of named volumes and make verify with list
volNames := []string{"homer", "bart", "lisa", "maggie", "marge"}
for i := 0; i < 5; i++ {
- _, err = volumes.Create(connText, handlers.VolumeCreateConfig{Name: volNames[i]})
+ _, err = volumes.Create(connText, entities.VolumeCreateOptions{Name: volNames[i]})
Expect(err).To(BeNil())
}
vols, err = volumes.List(connText, nil)
@@ -152,15 +151,15 @@ var _ = Describe("Podman volumes", func() {
Expect(err).To(BeNil())
// Removing an unused volume should work
- _, err = volumes.Create(connText, handlers.VolumeCreateConfig{})
+ _, err = volumes.Create(connText, entities.VolumeCreateOptions{})
Expect(err).To(BeNil())
vols, err := volumes.Prune(connText)
Expect(err).To(BeNil())
Expect(len(vols)).To(BeNumerically("==", 1))
- _, err = volumes.Create(connText, handlers.VolumeCreateConfig{Name: "homer"})
+ _, err = volumes.Create(connText, entities.VolumeCreateOptions{Name: "homer"})
Expect(err).To(BeNil())
- _, err = volumes.Create(connText, handlers.VolumeCreateConfig{})
+ _, err = volumes.Create(connText, entities.VolumeCreateOptions{})
Expect(err).To(BeNil())
session := bt.runPodman([]string{"run", "-dt", "-v", fmt.Sprintf("%s:/homer", "homer"), "--name", "vtest", alpine.name, "top"})
session.Wait(45)
diff --git a/pkg/bindings/volumes/volumes.go b/pkg/bindings/volumes/volumes.go
index 0bc818605..a2164e0af 100644
--- a/pkg/bindings/volumes/volumes.go
+++ b/pkg/bindings/volumes/volumes.go
@@ -8,15 +8,15 @@ import (
"strings"
"github.com/containers/libpod/libpod"
- "github.com/containers/libpod/pkg/api/handlers"
"github.com/containers/libpod/pkg/bindings"
+ "github.com/containers/libpod/pkg/domain/entities"
jsoniter "github.com/json-iterator/go"
)
// Create creates a volume given its configuration.
-func Create(ctx context.Context, config handlers.VolumeCreateConfig) (*libpod.VolumeConfig, error) {
+func Create(ctx context.Context, config entities.VolumeCreateOptions) (*entities.VolumeConfigResponse, error) {
var (
- v libpod.VolumeConfig
+ v entities.VolumeConfigResponse
)
conn, err := bindings.GetClient(ctx)
if err != nil {
diff --git a/pkg/domain/entities/engine_container.go b/pkg/domain/entities/engine_container.go
index aa2ceb630..c79e96391 100644
--- a/pkg/domain/entities/engine_container.go
+++ b/pkg/domain/entities/engine_container.go
@@ -11,6 +11,7 @@ type ContainerEngine interface {
ContainerWait(ctx context.Context, namesOrIds []string, options WaitOptions) ([]WaitReport, error)
PodDelete(ctx context.Context, opts PodPruneOptions) (*PodDeleteReport, error)
PodPrune(ctx context.Context) (*PodPruneReport, error)
+ VolumeCreate(ctx context.Context, opts VolumeCreateOptions) (*IdOrNameResponse, error)
VolumeDelete(ctx context.Context, opts VolumeDeleteOptions) (*VolumeDeleteReport, error)
VolumePrune(ctx context.Context) (*VolumePruneReport, error)
}
diff --git a/pkg/domain/entities/volumes.go b/pkg/domain/entities/volumes.go
new file mode 100644
index 000000000..ad12d0d01
--- /dev/null
+++ b/pkg/domain/entities/volumes.go
@@ -0,0 +1,41 @@
+package entities
+
+import "time"
+
+// swagger:model VolumeCreate
+type VolumeCreateOptions struct {
+ // New volume's name. Can be left blank
+ Name string `schema:"name"`
+ // Volume driver to use
+ Driver string `schema:"driver"`
+ // User-defined key/value metadata.
+ Label map[string]string `schema:"label"`
+ // Mapping of driver options and values.
+ Options map[string]string `schema:"opts"`
+}
+
+type IdOrNameResponse struct {
+ // The Id or Name of an object
+ IdOrName string
+}
+
+type VolumeConfigResponse struct {
+ // Name of the volume.
+ Name string `json:"name"`
+ Labels map[string]string `json:"labels"`
+ // The volume driver. Empty string or local does not activate a volume
+ // driver, all other volumes will.
+ Driver string `json:"volumeDriver"`
+ // The location the volume is mounted at.
+ MountPoint string `json:"mountPoint"`
+ // Time the volume was created.
+ CreatedTime time.Time `json:"createdAt,omitempty"`
+ // Options to pass to the volume driver. For the local driver, this is
+ // a list of mount options. For other drivers, they are passed to the
+ // volume driver handling the volume.
+ Options map[string]string `json:"volumeOptions,omitempty"`
+ // UID the volume will be created as.
+ UID int `json:"uid"`
+ // GID the volume will be created as.
+ GID int `json:"gid"`
+}
diff --git a/pkg/domain/infra/abi/parse/parse.go b/pkg/domain/infra/abi/parse/parse.go
new file mode 100644
index 000000000..6c0e1ee55
--- /dev/null
+++ b/pkg/domain/infra/abi/parse/parse.go
@@ -0,0 +1,68 @@
+package parse
+
+import (
+ "strconv"
+ "strings"
+
+ "github.com/containers/libpod/libpod"
+ "github.com/containers/libpod/libpod/define"
+ "github.com/pkg/errors"
+ "github.com/sirupsen/logrus"
+)
+
+// Handle volume options from CLI.
+// Parse "o" option to find UID, GID.
+func ParseVolumeOptions(opts map[string]string) ([]libpod.VolumeCreateOption, error) {
+ libpodOptions := []libpod.VolumeCreateOption{}
+ volumeOptions := make(map[string]string)
+
+ for key, value := range opts {
+ switch key {
+ case "o":
+ // o has special handling to parse out UID, GID.
+ // These are separate Libpod options.
+ splitVal := strings.Split(value, ",")
+ finalVal := []string{}
+ for _, o := range splitVal {
+ // Options will be formatted as either "opt" or
+ // "opt=value"
+ splitO := strings.SplitN(o, "=", 2)
+ switch strings.ToLower(splitO[0]) {
+ case "uid":
+ if len(splitO) != 2 {
+ return nil, errors.Wrapf(define.ErrInvalidArg, "uid option must provide a UID")
+ }
+ intUID, err := strconv.Atoi(splitO[1])
+ if err != nil {
+ return nil, errors.Wrapf(err, "cannot convert UID %s to integer", splitO[1])
+ }
+ logrus.Debugf("Removing uid= from options and adding WithVolumeUID for UID %d", intUID)
+ libpodOptions = append(libpodOptions, libpod.WithVolumeUID(intUID))
+ case "gid":
+ if len(splitO) != 2 {
+ return nil, errors.Wrapf(define.ErrInvalidArg, "gid option must provide a GID")
+ }
+ intGID, err := strconv.Atoi(splitO[1])
+ if err != nil {
+ return nil, errors.Wrapf(err, "cannot convert GID %s to integer", splitO[1])
+ }
+ logrus.Debugf("Removing gid= from options and adding WithVolumeGID for GID %d", intGID)
+ libpodOptions = append(libpodOptions, libpod.WithVolumeGID(intGID))
+ default:
+ finalVal = append(finalVal, o)
+ }
+ }
+ if len(finalVal) > 0 {
+ volumeOptions[key] = strings.Join(finalVal, ",")
+ }
+ default:
+ volumeOptions[key] = value
+ }
+ }
+
+ if len(volumeOptions) > 0 {
+ libpodOptions = append(libpodOptions, libpod.WithVolumeOptions(volumeOptions))
+ }
+
+ return libpodOptions, nil
+}
diff --git a/pkg/domain/infra/abi/volumes.go b/pkg/domain/infra/abi/volumes.go
new file mode 100644
index 000000000..0783af441
--- /dev/null
+++ b/pkg/domain/infra/abi/volumes.go
@@ -0,0 +1,38 @@
+// +build ABISupport
+
+package abi
+
+import (
+ "context"
+
+ "github.com/containers/libpod/libpod"
+ "github.com/containers/libpod/pkg/domain/entities"
+ "github.com/containers/libpod/pkg/domain/infra/abi/parse"
+)
+
+func (ic *ContainerEngine) VolumeCreate(ctx context.Context, opts entities.VolumeCreateOptions) (*entities.IdOrNameResponse, error) {
+ var (
+ volumeOptions []libpod.VolumeCreateOption
+ )
+ if len(opts.Name) > 0 {
+ volumeOptions = append(volumeOptions, libpod.WithVolumeName(opts.Name))
+ }
+ if len(opts.Driver) > 0 {
+ volumeOptions = append(volumeOptions, libpod.WithVolumeDriver(opts.Driver))
+ }
+ if len(opts.Label) > 0 {
+ volumeOptions = append(volumeOptions, libpod.WithVolumeLabels(opts.Label))
+ }
+ if len(opts.Options) > 0 {
+ parsedOptions, err := parse.ParseVolumeOptions(opts.Options)
+ if err != nil {
+ return nil, err
+ }
+ volumeOptions = append(volumeOptions, parsedOptions...)
+ }
+ vol, err := ic.Libpod.NewVolume(ctx, volumeOptions...)
+ if err != nil {
+ return nil, err
+ }
+ return &entities.IdOrNameResponse{IdOrName: vol.Name()}, nil
+}
diff --git a/pkg/domain/infra/tunnel/volumes.go b/pkg/domain/infra/tunnel/volumes.go
new file mode 100644
index 000000000..49cf6a2f6
--- /dev/null
+++ b/pkg/domain/infra/tunnel/volumes.go
@@ -0,0 +1,16 @@
+package tunnel
+
+import (
+ "context"
+
+ "github.com/containers/libpod/pkg/bindings/volumes"
+ "github.com/containers/libpod/pkg/domain/entities"
+)
+
+func (ic *ContainerEngine) VolumeCreate(ctx context.Context, opts entities.VolumeCreateOptions) (*entities.IdOrNameResponse, error) {
+ response, err := volumes.Create(ic.ClientCxt, opts)
+ if err != nil {
+ return nil, err
+ }
+ return &entities.IdOrNameResponse{IdOrName: response.Name}, nil
+}