diff options
-rw-r--r-- | cmd/podmanV2/containers/exists.go | 1 | ||||
-rw-r--r-- | cmd/podmanV2/parse/parse.go | 188 | ||||
-rw-r--r-- | cmd/podmanV2/parse/parse_test.go | 152 | ||||
-rw-r--r-- | cmd/podmanV2/pods/exists.go | 43 | ||||
-rw-r--r-- | cmd/podmanV2/volumes/create.go | 72 | ||||
-rw-r--r-- | cmd/podmanV2/volumes/volume.go | 10 | ||||
-rw-r--r-- | pkg/api/handlers/libpod/volumes.go | 20 | ||||
-rw-r--r-- | pkg/api/handlers/types.go | 12 | ||||
-rw-r--r-- | pkg/api/server/swagger.go | 3 | ||||
-rw-r--r-- | pkg/bindings/test/volumes_test.go | 29 | ||||
-rw-r--r-- | pkg/bindings/volumes/volumes.go | 6 | ||||
-rw-r--r-- | pkg/domain/entities/engine_container.go | 2 | ||||
-rw-r--r-- | pkg/domain/entities/volumes.go | 41 | ||||
-rw-r--r-- | pkg/domain/infra/abi/parse/parse.go | 68 | ||||
-rw-r--r-- | pkg/domain/infra/abi/pods.go | 19 | ||||
-rw-r--r-- | pkg/domain/infra/abi/volumes.go | 38 | ||||
-rw-r--r-- | pkg/domain/infra/tunnel/pods.go | 13 | ||||
-rw-r--r-- | pkg/domain/infra/tunnel/volumes.go | 16 | ||||
-rw-r--r-- | pkg/specgen/pod.go | 140 | ||||
-rw-r--r-- | pkg/specgen/specgen.go | 2 |
20 files changed, 833 insertions, 42 deletions
diff --git a/cmd/podmanV2/containers/exists.go b/cmd/podmanV2/containers/exists.go index 3aff150be..22c798fcd 100644 --- a/cmd/podmanV2/containers/exists.go +++ b/cmd/podmanV2/containers/exists.go @@ -19,6 +19,7 @@ var ( Example: `podman container exists containerID podman container exists myctr || podman run --name myctr [etc...]`, RunE: exists, + Args: cobra.ExactArgs(1), } ) 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/pods/exists.go b/cmd/podmanV2/pods/exists.go new file mode 100644 index 000000000..e37f2ebd7 --- /dev/null +++ b/cmd/podmanV2/pods/exists.go @@ -0,0 +1,43 @@ +package pods + +import ( + "context" + "os" + + "github.com/containers/libpod/cmd/podmanV2/registry" + "github.com/containers/libpod/pkg/domain/entities" + "github.com/spf13/cobra" +) + +var ( + podExistsDescription = `If the named pod exists in local storage, podman pod exists exits with 0, otherwise the exit code will be 1.` + + existsCommand = &cobra.Command{ + Use: "exists POD", + Short: "Check if a pod exists in local storage", + Long: podExistsDescription, + RunE: exists, + Args: cobra.ExactArgs(1), + Example: `podman pod exists podID + podman pod exists mypod || podman pod create --name mypod`, + } +) + +func init() { + registry.Commands = append(registry.Commands, registry.CliCommand{ + Mode: []entities.EngineMode{entities.ABIMode, entities.TunnelMode}, + Command: existsCommand, + Parent: podCmd, + }) +} + +func exists(cmd *cobra.Command, args []string) error { + response, err := registry.ContainerEngine().PodExists(context.Background(), args[0]) + if err != nil { + return err + } + if !response.Value { + os.Exit(1) + } + return nil +} 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..5820c12c3 100644 --- a/pkg/domain/entities/engine_container.go +++ b/pkg/domain/entities/engine_container.go @@ -10,7 +10,9 @@ type ContainerEngine interface { ContainerExists(ctx context.Context, nameOrId string) (*BoolReport, error) ContainerWait(ctx context.Context, namesOrIds []string, options WaitOptions) ([]WaitReport, error) 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) } 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/pods.go b/pkg/domain/infra/abi/pods.go new file mode 100644 index 000000000..de22de68e --- /dev/null +++ b/pkg/domain/infra/abi/pods.go @@ -0,0 +1,19 @@ +// +build ABISupport + +package abi + +import ( + "context" + "github.com/pkg/errors" + + "github.com/containers/libpod/libpod/define" + "github.com/containers/libpod/pkg/domain/entities" +) + +func (ic *ContainerEngine) PodExists(ctx context.Context, nameOrId string) (*entities.BoolReport, error) { + _, err := ic.Libpod.LookupPod(nameOrId) + if err != nil && errors.Cause(err) != define.ErrNoSuchPod { + return nil, err + } + return &entities.BoolReport{Value: err == nil}, 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/pods.go b/pkg/domain/infra/tunnel/pods.go new file mode 100644 index 000000000..500069d51 --- /dev/null +++ b/pkg/domain/infra/tunnel/pods.go @@ -0,0 +1,13 @@ +package tunnel + +import ( + "context" + + "github.com/containers/libpod/pkg/bindings/pods" + "github.com/containers/libpod/pkg/domain/entities" +) + +func (ic *ContainerEngine) PodExists(ctx context.Context, nameOrId string) (*entities.BoolReport, error) { + exists, err := pods.Exists(ic.ClientCxt, nameOrId) + return &entities.BoolReport{Value: exists}, err +} 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 +} diff --git a/pkg/specgen/pod.go b/pkg/specgen/pod.go new file mode 100644 index 000000000..1aada83c4 --- /dev/null +++ b/pkg/specgen/pod.go @@ -0,0 +1,140 @@ +package specgen + +import ( + "net" + + "github.com/cri-o/ocicni/pkg/ocicni" +) + +// PodBasicConfig contains basic configuration options for pods. +type PodBasicConfig struct { + // Name is the name of the pod. + // If not provided, a name will be generated when the pod is created. + // Optional. + Name string `json:"name,omitempty"` + // Hostname is the pod's hostname. If not set, the name of the pod will + // be used (if a name was not provided here, the name auto-generated for + // the pod will be used). This will be used by the infra container and + // all containers in the pod as long as the UTS namespace is shared. + // Optional. + Hostname string `json:"hostname,omitempty"` + // Labels are key-value pairs that are used to add metadata to pods. + // Optional. + Labels map[string]string `json:"labels,omitempty"` + // NoInfra tells the pod not to create an infra container. If this is + // done, many networking-related options will become unavailable. + // Conflicts with setting any options in PodNetworkConfig, and the + // InfraCommand and InfraImages in this struct. + // Optional. + NoInfra bool `json:"no_infra,omitempty"` + // InfraCommand sets the command that will be used to start the infra + // container. + // If not set, the default set in the Libpod configuration file will be + // used. + // Conflicts with NoInfra=true. + // Optional. + InfraCommand []string `json:"infra_command,omitempty"` + // InfraImage is the image that will be used for the infra container. + // If not set, the default set in the Libpod configuration file will be + // used. + // Conflicts with NoInfra=true. + // Optional. + InfraImage string `json:"infra_image,omitempty"` + // SharedNamespaces instructs the pod to share a set of namespaces. + // Shared namespaces will be joined (by default) by every container + // which joins the pod. + // If not set and NoInfra is false, the pod will set a default set of + // namespaces to share. + // Conflicts with NoInfra=true. + // Optional. + SharedNamespaces []string `json:"shared_namespaces,omitempty"` +} + +// PodNetworkConfig contains networking configuration for a pod. +type PodNetworkConfig struct { + // NetNS is the configuration to use for the infra container's network + // namespace. This network will, by default, be shared with all + // containers in the pod. + // Cannot be set to FromContainer and FromPod. + // Setting this to anything except "" conflicts with NoInfra=true. + // Defaults to Bridge as root and Slirp as rootless. + // Mandatory. + NetNS Namespace `json:"netns,omitempty"` + // StaticIP sets a static IP for the infra container. As the infra + // container's network is used for the entire pod by default, this will + // thus be a static IP for the whole pod. + // Only available if NetNS is set to Bridge (the default for root). + // As such, conflicts with NoInfra=true by proxy. + // Optional. + StaticIP *net.IP `json:"static_ip,omitempty"` + // StaticMAC sets a static MAC for the infra container. As the infra + // container's network is used for the entire pod by default, this will + // thus be a static MAC for the entire pod. + // Only available if NetNS is set to Bridge (the default for root). + // As such, conflicts with NoInfra=true by proxy. + // Optional. + StaticMAC *net.HardwareAddr `json:"static_mac,omitempty"` + // PortMappings is a set of ports to map into the infra container. + // As, by default, containers share their network with the infra + // container, this will forward the ports to the entire pod. + // Only available if NetNS is set to Bridge or Slirp. + // Optional. + PortMappings []ocicni.PortMapping `json:"portmappings,omitempty"` + // CNINetworks is a list of CNI networks that the infra container will + // join. As, by default, containers share their network with the infra + // container, these networks will effectively be joined by the + // entire pod. + // Only available when NetNS is set to Bridge, the default for root. + // Optional. + CNINetworks []string `json:"cni_networks,omitempty"` + // NoManageResolvConf indicates that /etc/resolv.conf should not be + // managed by the pod. Instead, each container will create and manage a + // separate resolv.conf as if they had not joined a pod. + // Conflicts with NoInfra=true and DNSServer, DNSSearch, DNSOption. + // Optional. + NoManageResolvConf bool `json:"no_manage_resolv_conf,omitempty"` + // DNSServer is a set of DNS servers that will be used in the infra + // container's resolv.conf, which will, by default, be shared with all + // containers in the pod. + // If not provided, the host's DNS servers will be used, unless the only + // server set is a localhost address. As the container cannot connect to + // the host's localhost, a default server will instead be set. + // Conflicts with NoInfra=true. + // Optional. + DNSServer []net.IP `json:"dns_server,omitempty"` + // DNSSearch is a set of DNS search domains that will be used in the + // infra container's resolv.conf, which will, by default, be shared with + // all containers in the pod. + // If not provided, DNS search domains from the host's resolv.conf will + // be used. + // Conflicts with NoInfra=true. + // Optional. + DNSSearch []string `json:"dns_search,omitempty"` + // DNSOption is a set of DNS options that will be used in the infra + // container's resolv.conf, which will, by default, be shared with all + // containers in the pod. + // Conflicts with NoInfra=true. + // Optional. + DNSOption []string `json:"dns_option,omitempty"` + // NoManageHosts indicates that /etc/hosts should not be managed by the + // pod. Instead, each container will create a separate /etc/hosts as + // they would if not in a pod. + // Conflicts with HostAdd. + NoManageHosts bool `json:"no_manage_hosts,omitempty"` + // HostAdd is a set of hosts that will be added to the infra container's + // /etc/hosts that will, by default, be shared with all containers in + // the pod. + // Conflicts with NoInfra=true and NoManageHosts. + // Optional. + HostAdd []string `json:"hostadd,omitempty"` +} + +// PodCgroupConfig contains configuration options about a pod's cgroups. +// This will be expanded in future updates to pods. +type PodCgroupConfig struct { + // CgroupParent is the parent for the CGroup that the pod will create. + // This pod cgroup will, in turn, be the default cgroup parent for all + // containers in the pod. + // Optional. + CgroupParent string `json:"cgroup_parent,omitempty"` +} diff --git a/pkg/specgen/specgen.go b/pkg/specgen/specgen.go index 7a430652a..b123c1da5 100644 --- a/pkg/specgen/specgen.go +++ b/pkg/specgen/specgen.go @@ -53,7 +53,7 @@ type ContainerBasicConfig struct { Terminal bool `json:"terminal,omitempty"` // Stdin is whether the container will keep its STDIN open. Stdin bool `json:"stdin,omitempty"` - // Labels are key-valid labels that are used to add metadata to + // Labels are key-value pairs that are used to add metadata to // containers. // Optional. Labels map[string]string `json:"labels,omitempty"` |