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 (
|
import (
|
||||||
"github.com/containers/libpod/cmd/podmanV2/registry"
|
"github.com/containers/libpod/cmd/podmanV2/registry"
|
||||||
|
@ -8,7 +8,7 @@ import (
|
||||||
|
|
||||||
var (
|
var (
|
||||||
// Command: podman _volume_
|
// Command: podman _volume_
|
||||||
cmd = &cobra.Command{
|
volumeCmd = &cobra.Command{
|
||||||
Use: "volume",
|
Use: "volume",
|
||||||
Short: "Manage volumes",
|
Short: "Manage volumes",
|
||||||
Long: "Volumes are created in and can be shared between containers",
|
Long: "Volumes are created in and can be shared between containers",
|
||||||
|
@ -21,10 +21,10 @@ var (
|
||||||
func init() {
|
func init() {
|
||||||
registry.Commands = append(registry.Commands, registry.CliCommand{
|
registry.Commands = append(registry.Commands, registry.CliCommand{
|
||||||
Mode: []entities.EngineMode{entities.ABIMode, entities.TunnelMode},
|
Mode: []entities.EngineMode{entities.ABIMode, entities.TunnelMode},
|
||||||
Command: cmd,
|
Command: volumeCmd,
|
||||||
})
|
})
|
||||||
cmd.SetHelpTemplate(registry.HelpTemplate())
|
volumeCmd.SetHelpTemplate(registry.HelpTemplate())
|
||||||
cmd.SetUsageTemplate(registry.UsageTemplate())
|
volumeCmd.SetUsageTemplate(registry.UsageTemplate())
|
||||||
}
|
}
|
||||||
|
|
||||||
func preRunE(cmd *cobra.Command, args []string) error {
|
func preRunE(cmd *cobra.Command, args []string) error {
|
||||||
|
|
|
@ -8,8 +8,8 @@ import (
|
||||||
"github.com/containers/libpod/cmd/podman/shared"
|
"github.com/containers/libpod/cmd/podman/shared"
|
||||||
"github.com/containers/libpod/libpod"
|
"github.com/containers/libpod/libpod"
|
||||||
"github.com/containers/libpod/libpod/define"
|
"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/api/handlers/utils"
|
||||||
|
"github.com/containers/libpod/pkg/domain/entities"
|
||||||
"github.com/gorilla/schema"
|
"github.com/gorilla/schema"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
|
@ -25,7 +25,7 @@ func CreateVolume(w http.ResponseWriter, r *http.Request) {
|
||||||
}{
|
}{
|
||||||
// override any golang type defaults
|
// override any golang type defaults
|
||||||
}
|
}
|
||||||
input := handlers.VolumeCreateConfig{}
|
input := entities.VolumeCreateOptions{}
|
||||||
if err := decoder.Decode(&query, r.URL.Query()); err != nil {
|
if err := decoder.Decode(&query, r.URL.Query()); err != nil {
|
||||||
utils.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest,
|
utils.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest,
|
||||||
errors.Wrapf(err, "Failed to parse parameters for %s", r.URL.String()))
|
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 {
|
if len(input.Label) > 0 {
|
||||||
volumeOptions = append(volumeOptions, libpod.WithVolumeLabels(input.Label))
|
volumeOptions = append(volumeOptions, libpod.WithVolumeLabels(input.Label))
|
||||||
}
|
}
|
||||||
if len(input.Opts) > 0 {
|
if len(input.Options) > 0 {
|
||||||
parsedOptions, err := shared.ParseVolumeOptions(input.Opts)
|
parsedOptions, err := shared.ParseVolumeOptions(input.Options)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
utils.InternalServerError(w, err)
|
utils.InternalServerError(w, err)
|
||||||
return
|
return
|
||||||
|
@ -64,7 +64,17 @@ func CreateVolume(w http.ResponseWriter, r *http.Request) {
|
||||||
utils.InternalServerError(w, err)
|
utils.InternalServerError(w, err)
|
||||||
return
|
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) {
|
func InspectVolume(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
|
@ -128,18 +128,6 @@ type CreateContainerConfig struct {
|
||||||
NetworkingConfig dockerNetwork.NetworkingConfig
|
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
|
// swagger:model IDResponse
|
||||||
type IDResponse struct {
|
type IDResponse struct {
|
||||||
// ID
|
// ID
|
||||||
|
|
|
@ -4,6 +4,7 @@ import (
|
||||||
"github.com/containers/libpod/libpod"
|
"github.com/containers/libpod/libpod"
|
||||||
"github.com/containers/libpod/pkg/api/handlers"
|
"github.com/containers/libpod/pkg/api/handlers"
|
||||||
"github.com/containers/libpod/pkg/api/handlers/utils"
|
"github.com/containers/libpod/pkg/api/handlers/utils"
|
||||||
|
"github.com/containers/libpod/pkg/domain/entities"
|
||||||
)
|
)
|
||||||
|
|
||||||
// No such image
|
// No such image
|
||||||
|
@ -155,7 +156,7 @@ type ok struct {
|
||||||
type swagVolumeCreateResponse struct {
|
type swagVolumeCreateResponse struct {
|
||||||
// in:body
|
// in:body
|
||||||
Body struct {
|
Body struct {
|
||||||
libpod.VolumeConfig
|
entities.VolumeConfigResponse
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -6,11 +6,10 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"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/containers"
|
||||||
"github.com/containers/libpod/pkg/bindings/volumes"
|
"github.com/containers/libpod/pkg/bindings/volumes"
|
||||||
|
"github.com/containers/libpod/pkg/domain/entities"
|
||||||
"github.com/containers/libpod/pkg/bindings"
|
|
||||||
. "github.com/onsi/ginkgo"
|
. "github.com/onsi/ginkgo"
|
||||||
. "github.com/onsi/gomega"
|
. "github.com/onsi/gomega"
|
||||||
"github.com/onsi/gomega/gexec"
|
"github.com/onsi/gomega/gexec"
|
||||||
|
@ -53,13 +52,13 @@ var _ = Describe("Podman volumes", func() {
|
||||||
|
|
||||||
It("create volume", func() {
|
It("create volume", func() {
|
||||||
// create a volume with blank config should work
|
// create a volume with blank config should work
|
||||||
_, err := volumes.Create(connText, handlers.VolumeCreateConfig{})
|
_, err := volumes.Create(connText, entities.VolumeCreateOptions{})
|
||||||
Expect(err).To(BeNil())
|
Expect(err).To(BeNil())
|
||||||
|
|
||||||
vcc := handlers.VolumeCreateConfig{
|
vcc := entities.VolumeCreateOptions{
|
||||||
Name: "foobar",
|
Name: "foobar",
|
||||||
Label: nil,
|
Label: nil,
|
||||||
Opts: nil,
|
Options: nil,
|
||||||
}
|
}
|
||||||
vol, err := volumes.Create(connText, vcc)
|
vol, err := volumes.Create(connText, vcc)
|
||||||
Expect(err).To(BeNil())
|
Expect(err).To(BeNil())
|
||||||
|
@ -73,7 +72,7 @@ var _ = Describe("Podman volumes", func() {
|
||||||
})
|
})
|
||||||
|
|
||||||
It("inspect volume", func() {
|
It("inspect volume", func() {
|
||||||
vol, err := volumes.Create(connText, handlers.VolumeCreateConfig{})
|
vol, err := volumes.Create(connText, entities.VolumeCreateOptions{})
|
||||||
Expect(err).To(BeNil())
|
Expect(err).To(BeNil())
|
||||||
data, err := volumes.Inspect(connText, vol.Name)
|
data, err := volumes.Inspect(connText, vol.Name)
|
||||||
Expect(err).To(BeNil())
|
Expect(err).To(BeNil())
|
||||||
|
@ -87,13 +86,13 @@ var _ = Describe("Podman volumes", func() {
|
||||||
Expect(code).To(BeNumerically("==", http.StatusNotFound))
|
Expect(code).To(BeNumerically("==", http.StatusNotFound))
|
||||||
|
|
||||||
// Removing an unused volume should work
|
// Removing an unused volume should work
|
||||||
vol, err := volumes.Create(connText, handlers.VolumeCreateConfig{})
|
vol, err := volumes.Create(connText, entities.VolumeCreateOptions{})
|
||||||
Expect(err).To(BeNil())
|
Expect(err).To(BeNil())
|
||||||
err = volumes.Remove(connText, vol.Name, nil)
|
err = volumes.Remove(connText, vol.Name, nil)
|
||||||
Expect(err).To(BeNil())
|
Expect(err).To(BeNil())
|
||||||
|
|
||||||
// Removing a volume that is being used without force should be 409
|
// 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())
|
Expect(err).To(BeNil())
|
||||||
session := bt.runPodman([]string{"run", "-dt", "-v", fmt.Sprintf("%s:/foobar", vol.Name), "--name", "vtest", alpine.name, "top"})
|
session := bt.runPodman([]string{"run", "-dt", "-v", fmt.Sprintf("%s:/foobar", vol.Name), "--name", "vtest", alpine.name, "top"})
|
||||||
session.Wait(45)
|
session.Wait(45)
|
||||||
|
@ -119,7 +118,7 @@ var _ = Describe("Podman volumes", func() {
|
||||||
// create a bunch of named volumes and make verify with list
|
// create a bunch of named volumes and make verify with list
|
||||||
volNames := []string{"homer", "bart", "lisa", "maggie", "marge"}
|
volNames := []string{"homer", "bart", "lisa", "maggie", "marge"}
|
||||||
for i := 0; i < 5; i++ {
|
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())
|
Expect(err).To(BeNil())
|
||||||
}
|
}
|
||||||
vols, err = volumes.List(connText, nil)
|
vols, err = volumes.List(connText, nil)
|
||||||
|
@ -152,15 +151,15 @@ var _ = Describe("Podman volumes", func() {
|
||||||
Expect(err).To(BeNil())
|
Expect(err).To(BeNil())
|
||||||
|
|
||||||
// Removing an unused volume should work
|
// Removing an unused volume should work
|
||||||
_, err = volumes.Create(connText, handlers.VolumeCreateConfig{})
|
_, err = volumes.Create(connText, entities.VolumeCreateOptions{})
|
||||||
Expect(err).To(BeNil())
|
Expect(err).To(BeNil())
|
||||||
vols, err := volumes.Prune(connText)
|
vols, err := volumes.Prune(connText)
|
||||||
Expect(err).To(BeNil())
|
Expect(err).To(BeNil())
|
||||||
Expect(len(vols)).To(BeNumerically("==", 1))
|
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())
|
Expect(err).To(BeNil())
|
||||||
_, err = volumes.Create(connText, handlers.VolumeCreateConfig{})
|
_, err = volumes.Create(connText, entities.VolumeCreateOptions{})
|
||||||
Expect(err).To(BeNil())
|
Expect(err).To(BeNil())
|
||||||
session := bt.runPodman([]string{"run", "-dt", "-v", fmt.Sprintf("%s:/homer", "homer"), "--name", "vtest", alpine.name, "top"})
|
session := bt.runPodman([]string{"run", "-dt", "-v", fmt.Sprintf("%s:/homer", "homer"), "--name", "vtest", alpine.name, "top"})
|
||||||
session.Wait(45)
|
session.Wait(45)
|
||||||
|
|
|
@ -8,15 +8,15 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/containers/libpod/libpod"
|
"github.com/containers/libpod/libpod"
|
||||||
"github.com/containers/libpod/pkg/api/handlers"
|
|
||||||
"github.com/containers/libpod/pkg/bindings"
|
"github.com/containers/libpod/pkg/bindings"
|
||||||
|
"github.com/containers/libpod/pkg/domain/entities"
|
||||||
jsoniter "github.com/json-iterator/go"
|
jsoniter "github.com/json-iterator/go"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Create creates a volume given its configuration.
|
// 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 (
|
var (
|
||||||
v libpod.VolumeConfig
|
v entities.VolumeConfigResponse
|
||||||
)
|
)
|
||||||
conn, err := bindings.GetClient(ctx)
|
conn, err := bindings.GetClient(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -12,6 +12,7 @@ type ContainerEngine interface {
|
||||||
PodDelete(ctx context.Context, opts PodPruneOptions) (*PodDeleteReport, error)
|
PodDelete(ctx context.Context, opts PodPruneOptions) (*PodDeleteReport, error)
|
||||||
PodExists(ctx context.Context, nameOrId string) (*BoolReport, error)
|
PodExists(ctx context.Context, nameOrId string) (*BoolReport, error)
|
||||||
PodPrune(ctx context.Context) (*PodPruneReport, error)
|
PodPrune(ctx context.Context) (*PodPruneReport, error)
|
||||||
|
VolumeCreate(ctx context.Context, opts VolumeCreateOptions) (*IdOrNameResponse, error)
|
||||||
VolumeDelete(ctx context.Context, opts VolumeDeleteOptions) (*VolumeDeleteReport, error)
|
VolumeDelete(ctx context.Context, opts VolumeDeleteOptions) (*VolumeDeleteReport, error)
|
||||||
VolumePrune(ctx context.Context) (*VolumePruneReport, 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