mirror of https://github.com/containers/podman.git
Merge pull request #5577 from baude/v2volumecreate
podmanv2 volume create
This commit is contained in:
commit
195a82ffbc
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -12,6 +12,7 @@ type ContainerEngine interface {
|
|||
PodDelete(ctx context.Context, opts PodPruneOptions) (*PodDeleteReport, error)
|
||||
PodExists(ctx context.Context, nameOrId string) (*BoolReport, 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)
|
||||
}
|
||||
|
|
|
@ -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"`
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
Loading…
Reference in New Issue