diff options
53 files changed, 1429 insertions, 292 deletions
diff --git a/.gitignore b/.gitignore index 6ebb899cf..ea154fe5d 100644 --- a/.gitignore +++ b/.gitignore @@ -29,3 +29,4 @@ podman*.tar.gz contrib/spec/podman.spec *.rpm *.coverprofile +/cmd/podmanV2/podmanV2 diff --git a/cmd/podman/main_local_unsupported.go b/cmd/podman/main_local_unsupported.go new file mode 100644 index 000000000..75728627e --- /dev/null +++ b/cmd/podman/main_local_unsupported.go @@ -0,0 +1,44 @@ +// +build !remoteclient,!linux + +package main + +// The ONLY purpose of this file is to allow the subpackage to compile. Don’t expect anything +// to work. + +import ( + "syscall" + + "github.com/spf13/cobra" +) + +const remote = false + +func setSyslog() error { + return nil +} + +func profileOn(cmd *cobra.Command) error { + return nil +} + +func profileOff(cmd *cobra.Command) error { + return nil +} + +func setupRootless(cmd *cobra.Command, args []string) error { + return nil +} + +func setRLimits() error { + return nil +} + +func setUMask() { + // Be sure we can create directories with 0755 mode. + syscall.Umask(0022) +} + +// checkInput can be used to verify any of the globalopt values +func checkInput() error { + return nil +} diff --git a/cmd/podman/shared/funcs_linux_test.go b/cmd/podman/shared/funcs_linux_test.go new file mode 100644 index 000000000..88571153f --- /dev/null +++ b/cmd/podman/shared/funcs_linux_test.go @@ -0,0 +1,119 @@ +package shared + +import ( + "fmt" + "io/ioutil" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestGenerateCommand(t *testing.T) { + inputCommand := "docker run -it --name NAME -e NAME=NAME -e IMAGE=IMAGE IMAGE echo \"hello world\"" + correctCommand := "/proc/self/exe run -it --name bar -e NAME=bar -e IMAGE=foo foo echo hello world" + newCommand, err := GenerateCommand(inputCommand, "foo", "bar", "") + assert.Nil(t, err) + assert.Equal(t, "hello world", newCommand[11]) + assert.Equal(t, correctCommand, strings.Join(newCommand, " ")) +} + +func TestGenerateCommandCheckSubstitution(t *testing.T) { + type subsTest struct { + input string + expected string + shouldFail bool + } + + absTmpFile, err := ioutil.TempFile("", "podmanRunlabelTestAbsolutePath") + assert.Nil(t, err, "error creating tempfile") + defer os.Remove(absTmpFile.Name()) + + relTmpFile, err := ioutil.TempFile("./", "podmanRunlabelTestRelativePath") + assert.Nil(t, err, "error creating tempfile") + defer os.Remove(relTmpFile.Name()) + relTmpCmd, err := filepath.Abs(relTmpFile.Name()) + assert.Nil(t, err, "error getting absolute path for relative tmpfile") + + // this has a (low) potential of race conditions but no other way + removedTmpFile, err := ioutil.TempFile("", "podmanRunlabelTestRemove") + assert.Nil(t, err, "error creating tempfile") + os.Remove(removedTmpFile.Name()) + + absTmpCmd := fmt.Sprintf("%s --flag1 --flag2 --args=foo", absTmpFile.Name()) + tests := []subsTest{ + { + input: "docker run -it alpine:latest", + expected: "/proc/self/exe run -it alpine:latest", + shouldFail: false, + }, + { + input: "podman run -it alpine:latest", + expected: "/proc/self/exe run -it alpine:latest", + shouldFail: false, + }, + { + input: absTmpCmd, + expected: absTmpCmd, + shouldFail: false, + }, + { + input: "./" + relTmpFile.Name(), + expected: relTmpCmd, + shouldFail: false, + }, + { + input: "ls -la", + expected: "ls -la", + shouldFail: false, + }, + { + input: removedTmpFile.Name(), + expected: "", + shouldFail: true, + }, + } + + for _, test := range tests { + newCommand, err := GenerateCommand(test.input, "foo", "bar", "") + if test.shouldFail { + assert.NotNil(t, err) + } else { + assert.Nil(t, err) + } + assert.Equal(t, test.expected, strings.Join(newCommand, " ")) + } +} + +func TestGenerateCommandPath(t *testing.T) { + inputCommand := "docker run -it --name NAME -e NAME=NAME -e IMAGE=IMAGE IMAGE echo install" + correctCommand := "/proc/self/exe run -it --name bar -e NAME=bar -e IMAGE=foo foo echo install" + newCommand, _ := GenerateCommand(inputCommand, "foo", "bar", "") + assert.Equal(t, correctCommand, strings.Join(newCommand, " ")) +} + +func TestGenerateCommandNoSetName(t *testing.T) { + inputCommand := "docker run -it --name NAME -e NAME=NAME -e IMAGE=IMAGE IMAGE echo install" + correctCommand := "/proc/self/exe run -it --name foo -e NAME=foo -e IMAGE=foo foo echo install" + newCommand, err := GenerateCommand(inputCommand, "foo", "", "") + assert.Nil(t, err) + assert.Equal(t, correctCommand, strings.Join(newCommand, " ")) +} + +func TestGenerateCommandNoName(t *testing.T) { + inputCommand := "docker run -it -e IMAGE=IMAGE IMAGE echo install" + correctCommand := "/proc/self/exe run -it -e IMAGE=foo foo echo install" + newCommand, err := GenerateCommand(inputCommand, "foo", "", "") + assert.Nil(t, err) + assert.Equal(t, correctCommand, strings.Join(newCommand, " ")) +} + +func TestGenerateCommandAlreadyPodman(t *testing.T) { + inputCommand := "podman run -it --name NAME -e NAME=NAME -e IMAGE=IMAGE IMAGE echo install" + correctCommand := "/proc/self/exe run -it --name bar -e NAME=bar -e IMAGE=foo foo echo install" + newCommand, err := GenerateCommand(inputCommand, "foo", "bar", "") + assert.Nil(t, err) + assert.Equal(t, correctCommand, strings.Join(newCommand, " ")) +} diff --git a/cmd/podman/shared/funcs_test.go b/cmd/podman/shared/funcs_test.go index c05348242..dd856166e 100644 --- a/cmd/podman/shared/funcs_test.go +++ b/cmd/podman/shared/funcs_test.go @@ -1,11 +1,6 @@ package shared import ( - "fmt" - "io/ioutil" - "os" - "path/filepath" - "strings" "testing" "github.com/containers/libpod/pkg/util" @@ -17,113 +12,6 @@ var ( imageName = "bar" ) -func TestGenerateCommand(t *testing.T) { - inputCommand := "docker run -it --name NAME -e NAME=NAME -e IMAGE=IMAGE IMAGE echo \"hello world\"" - correctCommand := "/proc/self/exe run -it --name bar -e NAME=bar -e IMAGE=foo foo echo hello world" - newCommand, err := GenerateCommand(inputCommand, "foo", "bar", "") - assert.Nil(t, err) - assert.Equal(t, "hello world", newCommand[11]) - assert.Equal(t, correctCommand, strings.Join(newCommand, " ")) -} - -func TestGenerateCommandCheckSubstitution(t *testing.T) { - type subsTest struct { - input string - expected string - shouldFail bool - } - - absTmpFile, err := ioutil.TempFile("", "podmanRunlabelTestAbsolutePath") - assert.Nil(t, err, "error creating tempfile") - defer os.Remove(absTmpFile.Name()) - - relTmpFile, err := ioutil.TempFile("./", "podmanRunlabelTestRelativePath") - assert.Nil(t, err, "error creating tempfile") - defer os.Remove(relTmpFile.Name()) - relTmpCmd, err := filepath.Abs(relTmpFile.Name()) - assert.Nil(t, err, "error getting absolute path for relative tmpfile") - - // this has a (low) potential of race conditions but no other way - removedTmpFile, err := ioutil.TempFile("", "podmanRunlabelTestRemove") - assert.Nil(t, err, "error creating tempfile") - os.Remove(removedTmpFile.Name()) - - absTmpCmd := fmt.Sprintf("%s --flag1 --flag2 --args=foo", absTmpFile.Name()) - tests := []subsTest{ - { - input: "docker run -it alpine:latest", - expected: "/proc/self/exe run -it alpine:latest", - shouldFail: false, - }, - { - input: "podman run -it alpine:latest", - expected: "/proc/self/exe run -it alpine:latest", - shouldFail: false, - }, - { - input: absTmpCmd, - expected: absTmpCmd, - shouldFail: false, - }, - { - input: "./" + relTmpFile.Name(), - expected: relTmpCmd, - shouldFail: false, - }, - { - input: "ls -la", - expected: "ls -la", - shouldFail: false, - }, - { - input: removedTmpFile.Name(), - expected: "", - shouldFail: true, - }, - } - - for _, test := range tests { - newCommand, err := GenerateCommand(test.input, "foo", "bar", "") - if test.shouldFail { - assert.NotNil(t, err) - } else { - assert.Nil(t, err) - } - assert.Equal(t, test.expected, strings.Join(newCommand, " ")) - } -} - -func TestGenerateCommandPath(t *testing.T) { - inputCommand := "docker run -it --name NAME -e NAME=NAME -e IMAGE=IMAGE IMAGE echo install" - correctCommand := "/proc/self/exe run -it --name bar -e NAME=bar -e IMAGE=foo foo echo install" - newCommand, _ := GenerateCommand(inputCommand, "foo", "bar", "") - assert.Equal(t, correctCommand, strings.Join(newCommand, " ")) -} - -func TestGenerateCommandNoSetName(t *testing.T) { - inputCommand := "docker run -it --name NAME -e NAME=NAME -e IMAGE=IMAGE IMAGE echo install" - correctCommand := "/proc/self/exe run -it --name foo -e NAME=foo -e IMAGE=foo foo echo install" - newCommand, err := GenerateCommand(inputCommand, "foo", "", "") - assert.Nil(t, err) - assert.Equal(t, correctCommand, strings.Join(newCommand, " ")) -} - -func TestGenerateCommandNoName(t *testing.T) { - inputCommand := "docker run -it -e IMAGE=IMAGE IMAGE echo install" - correctCommand := "/proc/self/exe run -it -e IMAGE=foo foo echo install" - newCommand, err := GenerateCommand(inputCommand, "foo", "", "") - assert.Nil(t, err) - assert.Equal(t, correctCommand, strings.Join(newCommand, " ")) -} - -func TestGenerateCommandAlreadyPodman(t *testing.T) { - inputCommand := "podman run -it --name NAME -e NAME=NAME -e IMAGE=IMAGE IMAGE echo install" - correctCommand := "/proc/self/exe run -it --name bar -e NAME=bar -e IMAGE=foo foo echo install" - newCommand, err := GenerateCommand(inputCommand, "foo", "bar", "") - assert.Nil(t, err) - assert.Equal(t, correctCommand, strings.Join(newCommand, " ")) -} - func TestGenerateRunEnvironment(t *testing.T) { opts := make(map[string]string) opts["opt1"] = "one" diff --git a/cmd/podmanV2/containers/exists.go b/cmd/podmanV2/containers/exists.go new file mode 100644 index 000000000..22c798fcd --- /dev/null +++ b/cmd/podmanV2/containers/exists.go @@ -0,0 +1,43 @@ +package containers + +import ( + "context" + "os" + + "github.com/containers/libpod/cmd/podmanV2/registry" + "github.com/containers/libpod/pkg/domain/entities" + "github.com/spf13/cobra" +) + +var ( + containerExistsDescription = `If the named container exists in local storage, podman container exists exits with 0, otherwise the exit code will be 1.` + + existsCommand = &cobra.Command{ + Use: "exists CONTAINER", + Short: "Check if a container exists in local storage", + Long: containerExistsDescription, + Example: `podman container exists containerID + podman container exists myctr || podman run --name myctr [etc...]`, + RunE: exists, + Args: cobra.ExactArgs(1), + } +) + +func init() { + registry.Commands = append(registry.Commands, registry.CliCommand{ + Mode: []entities.EngineMode{entities.ABIMode, entities.TunnelMode}, + Command: existsCommand, + Parent: containerCmd, + }) +} + +func exists(cmd *cobra.Command, args []string) error { + response, err := registry.ContainerEngine().ContainerExists(context.Background(), args[0]) + if err != nil { + return err + } + if !response.Value { + os.Exit(1) + } + return nil +} diff --git a/cmd/podmanV2/containers/wait.go b/cmd/podmanV2/containers/wait.go new file mode 100644 index 000000000..27acb3348 --- /dev/null +++ b/cmd/podmanV2/containers/wait.go @@ -0,0 +1,82 @@ +package containers + +import ( + "context" + "fmt" + "time" + + "github.com/containers/libpod/cmd/podmanV2/registry" + "github.com/containers/libpod/libpod/define" + "github.com/containers/libpod/pkg/domain/entities" + "github.com/pkg/errors" + "github.com/spf13/cobra" +) + +var ( + waitDescription = `Block until one or more containers stop and then print their exit codes. +` + waitCommand = &cobra.Command{ + Use: "wait [flags] CONTAINER [CONTAINER...]", + Short: "Block on one or more containers", + Long: waitDescription, + RunE: wait, + Example: `podman wait --latest + podman wait --interval 5000 ctrID + podman wait ctrID1 ctrID2`, + } +) + +var ( + waitFlags = entities.WaitOptions{} + waitCondition string +) + +func init() { + registry.Commands = append(registry.Commands, registry.CliCommand{ + Mode: []entities.EngineMode{entities.ABIMode, entities.TunnelMode}, + Command: waitCommand, + Parent: containerCmd, + }) + + flags := waitCommand.Flags() + flags.DurationVarP(&waitFlags.Interval, "interval", "i", time.Duration(250), "Milliseconds to wait before polling for completion") + flags.BoolVarP(&waitFlags.Latest, "latest", "l", false, "Act on the latest container podman is aware of") + flags.StringVar(&waitCondition, "condition", "stopped", "Condition to wait on") + if registry.EngineOpts.EngineMode == entities.ABIMode { + // TODO: This is the same as V1. We could skip creating the flag altogether in V2... + _ = flags.MarkHidden("latest") + } +} + +func wait(cmd *cobra.Command, args []string) error { + var ( + err error + ) + if waitFlags.Latest && len(args) > 0 { + return errors.New("cannot combine latest flag and arguments") + } + if waitFlags.Interval == 0 { + return errors.New("interval must be greater then 0") + } + + waitFlags.Condition, err = define.StringToContainerStatus(waitCondition) + if err != nil { + return err + } + + responses, err := registry.ContainerEngine().ContainerWait(context.Background(), args, waitFlags) + if err != nil { + return err + } + for _, r := range responses { + if r.Error == nil { + fmt.Println(r.Id) + } + } + for _, r := range responses { + if r.Error != nil { + fmt.Println(err) + } + } + return nil +} diff --git a/cmd/podmanV2/images/inspect.go b/cmd/podmanV2/images/inspect.go index 9c44cea35..2ecbbb201 100644 --- a/cmd/podmanV2/images/inspect.go +++ b/cmd/podmanV2/images/inspect.go @@ -52,7 +52,7 @@ func init() { flags.BoolVarP(&inspectOpts.Size, "size", "s", false, "Display total file size") flags.StringVarP(&inspectOpts.Format, "format", "f", "", "Change the output format to a Go template") - if registry.GlobalFlags.EngineMode == entities.ABIMode { + if registry.EngineOpts.EngineMode == entities.ABIMode { // TODO: This is the same as V1. We could skip creating the flag altogether in V2... _ = flags.MarkHidden("latest") } diff --git a/cmd/podmanV2/main.go b/cmd/podmanV2/main.go index 0df086352..24f21d804 100644 --- a/cmd/podmanV2/main.go +++ b/cmd/podmanV2/main.go @@ -5,6 +5,7 @@ import ( "os" "reflect" "runtime" + "strings" _ "github.com/containers/libpod/cmd/podmanV2/containers" _ "github.com/containers/libpod/cmd/podmanV2/images" @@ -31,17 +32,19 @@ func initCobra() { case "darwin": fallthrough case "windows": - registry.GlobalFlags.EngineMode = entities.TunnelMode + registry.EngineOpts.EngineMode = entities.TunnelMode case "linux": - registry.GlobalFlags.EngineMode = entities.ABIMode + registry.EngineOpts.EngineMode = entities.ABIMode default: logrus.Errorf("%s is not a supported OS", runtime.GOOS) os.Exit(1) } // TODO: Is there a Cobra way to "peek" at os.Args? - if ok := Contains("--remote", os.Args); ok { - registry.GlobalFlags.EngineMode = entities.TunnelMode + for _, v := range os.Args { + if strings.HasPrefix(v, "--remote") { + registry.EngineOpts.EngineMode = entities.TunnelMode + } } cobra.OnInitialize(func() {}) @@ -50,7 +53,7 @@ func initCobra() { func main() { fmt.Fprintf(os.Stderr, "Number of commands: %d\n", len(registry.Commands)) for _, c := range registry.Commands { - if Contains(registry.GlobalFlags.EngineMode, c.Mode) { + if Contains(registry.EngineOpts.EngineMode, c.Mode) { parent := rootCmd if c.Parent != nil { parent = c.Parent 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/registry/registry.go b/cmd/podmanV2/registry/registry.go index fa51d6535..793d520a8 100644 --- a/cmd/podmanV2/registry/registry.go +++ b/cmd/podmanV2/registry/registry.go @@ -14,11 +14,13 @@ type CliCommand struct { } var ( - Commands []CliCommand - GlobalFlags entities.EngineFlags + Commands []CliCommand + imageEngine entities.ImageEngine containerEngine entities.ContainerEngine - PodmanTunnel bool + + EngineOpts entities.EngineOptions + GlobalFlags entities.EngineFlags ) // HelpTemplate returns the help template for podman commands @@ -63,7 +65,8 @@ func ImageEngine() entities.ImageEngine { // NewImageEngine is a wrapper for building an ImageEngine to be used for PreRunE functions func NewImageEngine(cmd *cobra.Command, args []string) (entities.ImageEngine, error) { if imageEngine == nil { - engine, err := infra.NewImageEngine(GlobalFlags.EngineMode, entities.EngineOptions{}) + EngineOpts.FlagSet = cmd.Flags() + engine, err := infra.NewImageEngine(EngineOpts) if err != nil { return nil, err } @@ -79,7 +82,8 @@ func ContainerEngine() entities.ContainerEngine { // NewContainerEngine is a wrapper for building an ContainerEngine to be used for PreRunE functions func NewContainerEngine(cmd *cobra.Command, args []string) (entities.ContainerEngine, error) { if containerEngine == nil { - engine, err := infra.NewContainerEngine(GlobalFlags.EngineMode, entities.EngineOptions{}) + EngineOpts.FlagSet = cmd.Flags() + engine, err := infra.NewContainerEngine(EngineOpts) if err != nil { return nil, err } diff --git a/cmd/podmanV2/root.go b/cmd/podmanV2/root.go index 778184f28..24b083b9f 100644 --- a/cmd/podmanV2/root.go +++ b/cmd/podmanV2/root.go @@ -24,7 +24,8 @@ func init() { // Override default --help information of `--version` global flag} var dummyVersion bool rootCmd.PersistentFlags().BoolVarP(&dummyVersion, "version", "v", false, "Version of podman") - rootCmd.PersistentFlags().BoolVarP(®istry.PodmanTunnel, "remote", "r", false, "Access service via SSH tunnel") + rootCmd.PersistentFlags().StringVarP(®istry.EngineOpts.Uri, "remote", "r", "", "URL to access podman service") + rootCmd.PersistentFlags().StringSliceVar(®istry.EngineOpts.Identities, "identity", []string{}, "path to SSH identity file") } func Execute() { 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/libpod/container_top_unsupported.go b/libpod/container_top_unsupported.go index 382c98b54..12f6cbb6c 100644 --- a/libpod/container_top_unsupported.go +++ b/libpod/container_top_unsupported.go @@ -4,6 +4,12 @@ package libpod import "github.com/containers/libpod/libpod/define" +// Top gathers statistics about the running processes in a container. It returns a +// []string for output +func (c *Container) Top(descriptors []string) ([]string, error) { + return nil, define.ErrNotImplemented +} + // GetContainerPidInformation returns process-related data of all processes in // the container. The output data can be controlled via the `descriptors` // argument which expects format descriptors and supports all AIXformat diff --git a/libpod/lock/shm/shm_lock_test.go b/libpod/lock/shm/shm_lock_test.go index 830035881..41a150c59 100644 --- a/libpod/lock/shm/shm_lock_test.go +++ b/libpod/lock/shm/shm_lock_test.go @@ -1,3 +1,5 @@ +// +build linux + package shm import ( diff --git a/libpod/util_unsupported.go b/libpod/util_unsupported.go index 9a9a6eeb6..4c5616bd0 100644 --- a/libpod/util_unsupported.go +++ b/libpod/util_unsupported.go @@ -25,7 +25,7 @@ func assembleSystemdCgroupName(baseSlice, newSlice string) (string, error) { // LabelVolumePath takes a mount path for a volume and gives it an // selinux label of either shared or not -func LabelVolumePath(path string, shared bool) error { +func LabelVolumePath(path string) error { return define.ErrNotImplemented } diff --git a/pkg/adapter/terminal_unsupported.go b/pkg/adapter/terminal_unsupported.go new file mode 100644 index 000000000..3009f0a38 --- /dev/null +++ b/pkg/adapter/terminal_unsupported.go @@ -0,0 +1,23 @@ +// +build !linux + +package adapter + +import ( + "context" + "os" + + "github.com/containers/libpod/libpod" + "github.com/containers/libpod/libpod/define" +) + +// ExecAttachCtr execs and attaches to a container +func ExecAttachCtr(ctx context.Context, ctr *libpod.Container, tty, privileged bool, env map[string]string, cmd []string, user, workDir string, streams *libpod.AttachStreams, preserveFDs uint, detachKeys string) (int, error) { + return -1, define.ErrNotImplemented +} + +// StartAttachCtr starts and (if required) attaches to a container +// if you change the signature of this function from os.File to io.Writer, it will trigger a downstream +// error. we may need to just lint disable this one. +func StartAttachCtr(ctx context.Context, ctr *libpod.Container, stdout, stderr, stdin *os.File, detachKeys string, sigProxy bool, startContainer bool, recursive bool) error { //nolint-interfacer + return define.ErrNotImplemented +} diff --git a/pkg/api/handlers/libpod/containers.go b/pkg/api/handlers/libpod/containers.go index 8020c391d..cdc34004f 100644 --- a/pkg/api/handlers/libpod/containers.go +++ b/pkg/api/handlers/libpod/containers.go @@ -21,8 +21,12 @@ func ContainerExists(w http.ResponseWriter, r *http.Request) { name := utils.GetName(r) _, err := runtime.LookupContainer(name) if err != nil { - utils.ContainerNotFound(w, name, err) + if errors.Cause(err) == define.ErrNoSuchCtr { + utils.ContainerNotFound(w, name, err) + } + utils.InternalServerError(w, err) return + } utils.WriteResponse(w, http.StatusNoContent, "") } 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/connection.go b/pkg/bindings/connection.go index 289debd8c..4fe4dd72d 100644 --- a/pkg/bindings/connection.go +++ b/pkg/bindings/connection.go @@ -165,8 +165,13 @@ func sshClient(_url *url.URL, identity string, secure bool) (*http.Client, error } } + port := _url.Port() + if port == "" { + port = "22" + } + bastion, err := ssh.Dial("tcp", - net.JoinHostPort(_url.Hostname(), _url.Port()), + net.JoinHostPort(_url.Hostname(), port), &ssh.ClientConfig{ User: _url.User.Username(), Auth: []ssh.AuthMethod{auth}, diff --git a/pkg/bindings/containers/containers.go b/pkg/bindings/containers/containers.go index f298dbba1..534555a00 100644 --- a/pkg/bindings/containers/containers.go +++ b/pkg/bindings/containers/containers.go @@ -7,6 +7,7 @@ import ( "strconv" "github.com/containers/libpod/libpod" + "github.com/containers/libpod/libpod/define" lpapiv2 "github.com/containers/libpod/pkg/api/handlers/libpod" "github.com/containers/libpod/pkg/bindings" ) @@ -212,7 +213,7 @@ func Unpause(ctx context.Context, nameOrID string) error { // Wait blocks until the given container reaches a condition. If not provided, the condition will // default to stopped. If the condition is stopped, an exit code for the container will be provided. The // nameOrID can be a container name or a partial/full ID. -func Wait(ctx context.Context, nameOrID string, condition *string) (int32, error) { +func Wait(ctx context.Context, nameOrID string, condition *define.ContainerStatus) (int32, error) { //nolint var exitCode int32 conn, err := bindings.GetClient(ctx) if err != nil { @@ -220,7 +221,7 @@ func Wait(ctx context.Context, nameOrID string, condition *string) (int32, error } params := url.Values{} if condition != nil { - params.Set("condition", *condition) + params.Set("condition", condition.String()) } response, err := conn.DoRequest(nil, http.MethodPost, "/containers/%s/wait", params, nameOrID) if err != nil { diff --git a/pkg/bindings/test/common_test.go b/pkg/bindings/test/common_test.go index 409e620a3..6b8d6788c 100644 --- a/pkg/bindings/test/common_test.go +++ b/pkg/bindings/test/common_test.go @@ -3,6 +3,7 @@ package test_bindings import ( "context" "fmt" + "github.com/containers/libpod/libpod/define" "io/ioutil" "os" "os/exec" @@ -205,8 +206,8 @@ func (b *bindingTest) RunTopContainer(containerName *string, insidePod *bool, po if err != nil { return "", err } - waiting := "running" - _, err = containers.Wait(b.conn, ctr.ID, &waiting) + wait := define.ContainerStateRunning + _, err = containers.Wait(b.conn, ctr.ID, &wait) return ctr.ID, err } diff --git a/pkg/bindings/test/containers_test.go b/pkg/bindings/test/containers_test.go index afb0cc19b..f5465c803 100644 --- a/pkg/bindings/test/containers_test.go +++ b/pkg/bindings/test/containers_test.go @@ -1,6 +1,7 @@ package test_bindings import ( + "github.com/containers/libpod/libpod/define" "net/http" "strconv" "strings" @@ -282,8 +283,8 @@ var _ = Describe("Podman containers ", func() { var ( name = "top" exitCode int32 = -1 - pause = "paused" - unpause = "running" + pause = define.ContainerStatePaused + running = define.ContainerStateRunning ) errChan := make(chan error) _, err := bt.RunTopContainer(&name, nil, nil) @@ -301,7 +302,7 @@ var _ = Describe("Podman containers ", func() { errChan = make(chan error) go func() { - _, waitErr := containers.Wait(bt.conn, name, &unpause) + _, waitErr := containers.Wait(bt.conn, name, &running) errChan <- waitErr close(errChan) }() 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/containers.go b/pkg/domain/entities/containers.go index 1899b98f7..0e1208b3b 100644 --- a/pkg/domain/entities/containers.go +++ b/pkg/domain/entities/containers.go @@ -1 +1,23 @@ package entities + +import ( + "time" + + "github.com/containers/libpod/libpod/define" +) + +type WaitOptions struct { + Condition define.ContainerStatus + Interval time.Duration + Latest bool +} + +type WaitReport struct { + Id string + Error error + ExitCode int32 +} + +type BoolReport struct { + Value bool +} diff --git a/pkg/domain/entities/engine.go b/pkg/domain/entities/engine.go index a1096f1f1..08ef1df92 100644 --- a/pkg/domain/entities/engine.go +++ b/pkg/domain/entities/engine.go @@ -1,7 +1,6 @@ package entities import ( - "net/url" "os/user" "path/filepath" @@ -20,11 +19,13 @@ func (m EngineMode) String() string { return string(m) } +// FIXME: merge EngineOptions and EngineFlags type EngineOptions struct { - Uri *url.URL + Uri string Identities []string - FlagSet pflag.FlagSet + FlagSet *pflag.FlagSet Flags EngineFlags + EngineMode EngineMode } type EngineFlags struct { @@ -58,8 +59,6 @@ type EngineFlags struct { Port int IdentityFile string IgnoreHosts bool - - EngineMode EngineMode } func NewEngineOptions() (EngineFlags, error) { diff --git a/pkg/domain/entities/engine_container.go b/pkg/domain/entities/engine_container.go index d08f37d44..5820c12c3 100644 --- a/pkg/domain/entities/engine_container.go +++ b/pkg/domain/entities/engine_container.go @@ -5,22 +5,14 @@ import ( ) type ContainerEngine interface { - ContainerRuntime - PodRuntime - VolumeRuntime -} - -type ContainerRuntime interface { ContainerDelete(ctx context.Context, opts ContainerDeleteOptions) (*ContainerDeleteReport, error) ContainerPrune(ctx context.Context) (*ContainerPruneReport, error) -} - -type PodRuntime 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) -} - -type VolumeRuntime interface { + 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/containers.go b/pkg/domain/infra/abi/containers.go new file mode 100644 index 000000000..cdcd77246 --- /dev/null +++ b/pkg/domain/infra/abi/containers.go @@ -0,0 +1,66 @@ +// +build ABISupport + +package abi + +import ( + "context" + + "github.com/containers/libpod/libpod/define" + "github.com/containers/libpod/pkg/adapter/shortcuts" + "github.com/containers/libpod/pkg/domain/entities" + "github.com/pkg/errors" +) + +// TODO: Should return *entities.ContainerExistsReport, error +func (ic *ContainerEngine) ContainerExists(ctx context.Context, nameOrId string) (*entities.BoolReport, error) { + _, err := ic.Libpod.LookupContainer(nameOrId) + if err != nil && errors.Cause(err) != define.ErrNoSuchCtr { + return nil, err + } + return &entities.BoolReport{Value: err == nil}, nil +} + +func (ic *ContainerEngine) ContainerWait(ctx context.Context, namesOrIds []string, options entities.WaitOptions) ([]entities.WaitReport, error) { + var ( + responses []entities.WaitReport + ) + ctrs, err := shortcuts.GetContainersByContext(false, options.Latest, namesOrIds, ic.Libpod) + if err != nil { + return nil, err + } + for _, c := range ctrs { + response := entities.WaitReport{Id: c.ID()} + exitCode, err := c.WaitForConditionWithInterval(options.Interval, options.Condition) + if err != nil { + response.Error = err + } else { + response.ExitCode = exitCode + } + responses = append(responses, response) + } + return responses, nil +} + +func (ic *ContainerEngine) ContainerDelete(ctx context.Context, opts entities.ContainerDeleteOptions) (*entities.ContainerDeleteReport, error) { + panic("implement me") +} + +func (ic *ContainerEngine) ContainerPrune(ctx context.Context) (*entities.ContainerPruneReport, error) { + panic("implement me") +} + +func (ic *ContainerEngine) PodDelete(ctx context.Context, opts entities.PodPruneOptions) (*entities.PodDeleteReport, error) { + panic("implement me") +} + +func (ic *ContainerEngine) PodPrune(ctx context.Context) (*entities.PodPruneReport, error) { + panic("implement me") +} + +func (ic *ContainerEngine) VolumeDelete(ctx context.Context, opts entities.VolumeDeleteOptions) (*entities.VolumeDeleteReport, error) { + panic("implement me") +} + +func (ic *ContainerEngine) VolumePrune(ctx context.Context) (*entities.VolumePruneReport, error) { + panic("implement me") +} 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/runtime.go b/pkg/domain/infra/abi/runtime.go index 479a69586..b53fb6d3a 100644 --- a/pkg/domain/infra/abi/runtime.go +++ b/pkg/domain/infra/abi/runtime.go @@ -4,7 +4,6 @@ package abi import ( "github.com/containers/libpod/libpod" - "github.com/containers/libpod/pkg/domain/entities" ) // Image-related runtime linked against libpod library @@ -14,6 +13,5 @@ type ImageEngine struct { // Container-related runtime linked against libpod library type ContainerEngine struct { - entities.ContainerEngine Libpod *libpod.Runtime } 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/runtime_abi.go b/pkg/domain/infra/runtime_abi.go index de996f567..31f832423 100644 --- a/pkg/domain/infra/runtime_abi.go +++ b/pkg/domain/infra/runtime_abi.go @@ -8,32 +8,31 @@ import ( "github.com/containers/libpod/pkg/bindings" "github.com/containers/libpod/pkg/domain/entities" - "github.com/containers/libpod/pkg/domain/infra/abi" "github.com/containers/libpod/pkg/domain/infra/tunnel" ) // NewContainerEngine factory provides a libpod runtime for container-related operations -func NewContainerEngine(mode entities.EngineMode, opts entities.EngineOptions) (entities.ContainerEngine, error) { - switch mode { +func NewContainerEngine(opts entities.EngineOptions) (entities.ContainerEngine, error) { + switch opts.EngineMode { case entities.ABIMode: r, err := NewLibpodRuntime(opts.FlagSet, opts.Flags) - return &abi.ContainerEngine{ContainerEngine: r}, err + return r, err case entities.TunnelMode: - ctx, err := bindings.NewConnection(context.Background(), opts.Uri.String(), opts.Identities...) + ctx, err := bindings.NewConnection(context.Background(), opts.Uri, opts.Identities...) return &tunnel.ContainerEngine{ClientCxt: ctx}, err } - return nil, fmt.Errorf("runtime mode '%v' is not supported", mode) + return nil, fmt.Errorf("runtime mode '%v' is not supported", opts.EngineMode) } // NewContainerEngine factory provides a libpod runtime for image-related operations -func NewImageEngine(mode entities.EngineMode, opts entities.EngineOptions) (entities.ImageEngine, error) { - switch mode { +func NewImageEngine(opts entities.EngineOptions) (entities.ImageEngine, error) { + switch opts.EngineMode { case entities.ABIMode: r, err := NewLibpodImageRuntime(opts.FlagSet, opts.Flags) return r, err case entities.TunnelMode: - ctx, err := bindings.NewConnection(context.Background(), opts.Uri.String(), opts.Identities...) + ctx, err := bindings.NewConnection(context.Background(), opts.Uri, opts.Identities...) return &tunnel.ImageEngine{ClientCxt: ctx}, err } - return nil, fmt.Errorf("runtime mode '%v' is not supported", mode) + return nil, fmt.Errorf("runtime mode '%v' is not supported", opts.EngineMode) } diff --git a/pkg/domain/infra/runtime_image_proxy.go b/pkg/domain/infra/runtime_image_proxy.go index 480e7c8d7..d2e66c08c 100644 --- a/pkg/domain/infra/runtime_image_proxy.go +++ b/pkg/domain/infra/runtime_image_proxy.go @@ -12,14 +12,10 @@ import ( // ContainerEngine Image Proxy will be EOL'ed after podmanV2 is separated from libpod repo -func NewLibpodImageRuntime(flags pflag.FlagSet, opts entities.EngineFlags) (entities.ImageEngine, error) { +func NewLibpodImageRuntime(flags *pflag.FlagSet, opts entities.EngineFlags) (entities.ImageEngine, error) { r, err := GetRuntime(context.Background(), flags, opts) if err != nil { return nil, err } return &abi.ImageEngine{Libpod: r}, nil } - -func (ir *runtime) ShutdownImageRuntime(force bool) error { - return ir.Libpod.Shutdown(force) -} diff --git a/pkg/domain/infra/runtime_libpod.go b/pkg/domain/infra/runtime_libpod.go index b42c52b6e..b835152bf 100644 --- a/pkg/domain/infra/runtime_libpod.go +++ b/pkg/domain/infra/runtime_libpod.go @@ -26,7 +26,7 @@ type engineOpts struct { } // GetRuntimeMigrate gets a libpod runtime that will perform a migration of existing containers -func GetRuntimeMigrate(ctx context.Context, fs flag.FlagSet, ef entities.EngineFlags, newRuntime string) (*libpod.Runtime, error) { +func GetRuntimeMigrate(ctx context.Context, fs *flag.FlagSet, ef entities.EngineFlags, newRuntime string) (*libpod.Runtime, error) { return getRuntime(ctx, fs, &engineOpts{ name: newRuntime, renumber: false, @@ -38,7 +38,7 @@ func GetRuntimeMigrate(ctx context.Context, fs flag.FlagSet, ef entities.EngineF } // GetRuntimeDisableFDs gets a libpod runtime that will disable sd notify -func GetRuntimeDisableFDs(ctx context.Context, fs flag.FlagSet, ef entities.EngineFlags) (*libpod.Runtime, error) { +func GetRuntimeDisableFDs(ctx context.Context, fs *flag.FlagSet, ef entities.EngineFlags) (*libpod.Runtime, error) { return getRuntime(ctx, fs, &engineOpts{ renumber: false, migrate: false, @@ -49,7 +49,7 @@ func GetRuntimeDisableFDs(ctx context.Context, fs flag.FlagSet, ef entities.Engi } // GetRuntimeRenumber gets a libpod runtime that will perform a lock renumber -func GetRuntimeRenumber(ctx context.Context, fs flag.FlagSet, ef entities.EngineFlags) (*libpod.Runtime, error) { +func GetRuntimeRenumber(ctx context.Context, fs *flag.FlagSet, ef entities.EngineFlags) (*libpod.Runtime, error) { return getRuntime(ctx, fs, &engineOpts{ renumber: true, migrate: false, @@ -60,7 +60,7 @@ func GetRuntimeRenumber(ctx context.Context, fs flag.FlagSet, ef entities.Engine } // GetRuntime generates a new libpod runtime configured by command line options -func GetRuntime(ctx context.Context, flags flag.FlagSet, ef entities.EngineFlags) (*libpod.Runtime, error) { +func GetRuntime(ctx context.Context, flags *flag.FlagSet, ef entities.EngineFlags) (*libpod.Runtime, error) { return getRuntime(ctx, flags, &engineOpts{ renumber: false, migrate: false, @@ -71,7 +71,7 @@ func GetRuntime(ctx context.Context, flags flag.FlagSet, ef entities.EngineFlags } // GetRuntimeNoStore generates a new libpod runtime configured by command line options -func GetRuntimeNoStore(ctx context.Context, fs flag.FlagSet, ef entities.EngineFlags) (*libpod.Runtime, error) { +func GetRuntimeNoStore(ctx context.Context, fs *flag.FlagSet, ef entities.EngineFlags) (*libpod.Runtime, error) { return getRuntime(ctx, fs, &engineOpts{ renumber: false, migrate: false, @@ -81,7 +81,7 @@ func GetRuntimeNoStore(ctx context.Context, fs flag.FlagSet, ef entities.EngineF }) } -func getRuntime(ctx context.Context, fs flag.FlagSet, opts *engineOpts) (*libpod.Runtime, error) { +func getRuntime(ctx context.Context, fs *flag.FlagSet, opts *engineOpts) (*libpod.Runtime, error) { options := []libpod.RuntimeOption{} storageOpts := storage.StoreOptions{} storageSet := false diff --git a/pkg/domain/infra/runtime_proxy.go b/pkg/domain/infra/runtime_proxy.go index d17b8efa1..4095ae6e2 100644 --- a/pkg/domain/infra/runtime_proxy.go +++ b/pkg/domain/infra/runtime_proxy.go @@ -5,50 +5,17 @@ package infra import ( "context" - "github.com/containers/libpod/libpod" "github.com/containers/libpod/pkg/domain/entities" + "github.com/containers/libpod/pkg/domain/infra/abi" flag "github.com/spf13/pflag" ) // ContainerEngine Proxy will be EOL'ed after podmanV2 is separated from libpod repo -type runtime struct { - entities.ContainerEngine - Libpod *libpod.Runtime -} - -func NewLibpodRuntime(flags flag.FlagSet, opts entities.EngineFlags) (entities.ContainerEngine, error) { +func NewLibpodRuntime(flags *flag.FlagSet, opts entities.EngineFlags) (entities.ContainerEngine, error) { r, err := GetRuntime(context.Background(), flags, opts) if err != nil { return nil, err } - return &runtime{Libpod: r}, nil -} - -func (r *runtime) ShutdownRuntime(force bool) error { - return r.Libpod.Shutdown(force) -} - -func (r *runtime) ContainerDelete(ctx context.Context, opts entities.ContainerDeleteOptions) (*entities.ContainerDeleteReport, error) { - panic("implement me") -} - -func (r *runtime) ContainerPrune(ctx context.Context) (*entities.ContainerPruneReport, error) { - panic("implement me") -} - -func (r *runtime) PodDelete(ctx context.Context, opts entities.PodPruneOptions) (*entities.PodDeleteReport, error) { - panic("implement me") -} - -func (r *runtime) PodPrune(ctx context.Context) (*entities.PodPruneReport, error) { - panic("implement me") -} - -func (r *runtime) VolumeDelete(ctx context.Context, opts entities.VolumeDeleteOptions) (*entities.VolumeDeleteReport, error) { - panic("implement me") -} - -func (r *runtime) VolumePrune(ctx context.Context) (*entities.VolumePruneReport, error) { - panic("implement me") + return &abi.ContainerEngine{Libpod: r}, nil } diff --git a/pkg/domain/infra/runtime_tunnel.go b/pkg/domain/infra/runtime_tunnel.go index 8a606deaf..5816ef0c0 100644 --- a/pkg/domain/infra/runtime_tunnel.go +++ b/pkg/domain/infra/runtime_tunnel.go @@ -11,25 +11,25 @@ import ( "github.com/containers/libpod/pkg/domain/infra/tunnel" ) -func NewContainerEngine(mode entities.EngineMode, opts entities.EngineOptions) (entities.ContainerEngine, error) { - switch mode { +func NewContainerEngine(opts entities.EngineOptions) (entities.ContainerEngine, error) { + switch opts.EngineMode { case entities.ABIMode: return nil, fmt.Errorf("direct runtime not supported") case entities.TunnelMode: - ctx, err := bindings.NewConnection(context.Background(), opts.Uri.String(), opts.Identities...) + ctx, err := bindings.NewConnection(context.Background(), opts.Uri, opts.Identities...) return &tunnel.ContainerEngine{ClientCxt: ctx}, err } - return nil, fmt.Errorf("runtime mode '%v' is not supported", mode) + return nil, fmt.Errorf("runtime mode '%v' is not supported", opts.EngineMode) } // NewImageEngine factory provides a libpod runtime for image-related operations -func NewImageEngine(mode entities.EngineMode, opts entities.EngineOptions) (entities.ImageEngine, error) { - switch mode { +func NewImageEngine(opts entities.EngineOptions) (entities.ImageEngine, error) { + switch opts.EngineMode { case entities.ABIMode: return nil, fmt.Errorf("direct image runtime not supported") case entities.TunnelMode: - ctx, err := bindings.NewConnection(context.Background(), opts.Uri.String(), opts.Identities...) + ctx, err := bindings.NewConnection(context.Background(), opts.Uri, opts.Identities...) return &tunnel.ImageEngine{ClientCxt: ctx}, err } - return nil, fmt.Errorf("runtime mode '%v' is not supported", mode) + return nil, fmt.Errorf("runtime mode '%v' is not supported", opts.EngineMode) } diff --git a/pkg/domain/infra/tunnel/containers.go b/pkg/domain/infra/tunnel/containers.go new file mode 100644 index 000000000..8bf74126d --- /dev/null +++ b/pkg/domain/infra/tunnel/containers.go @@ -0,0 +1,42 @@ +package tunnel + +import ( + "context" + + "github.com/containers/libpod/pkg/bindings/containers" + "github.com/containers/libpod/pkg/domain/entities" +) + +func (ic *ContainerEngine) ContainerExists(ctx context.Context, nameOrId string) (*entities.BoolReport, error) { + exists, err := containers.Exists(ic.ClientCxt, nameOrId) + return &entities.BoolReport{Value: exists}, err +} + +func (ic *ContainerEngine) ContainerWait(ctx context.Context, namesOrIds []string, options entities.WaitOptions) ([]entities.WaitReport, error) { + var ( + responses []entities.WaitReport + ) + cons, err := getContainersByContext(ic.ClientCxt, false, namesOrIds) + if err != nil { + return nil, err + } + for _, c := range cons { + response := entities.WaitReport{Id: c.ID} + exitCode, err := containers.Wait(ic.ClientCxt, c.ID, &options.Condition) + if err != nil { + response.Error = err + } else { + response.ExitCode = exitCode + } + responses = append(responses, response) + } + return responses, nil +} + +func (r *ContainerEngine) ContainerDelete(ctx context.Context, opts entities.ContainerDeleteOptions) (*entities.ContainerDeleteReport, error) { + panic("implement me") +} + +func (r *ContainerEngine) ContainerPrune(ctx context.Context) (*entities.ContainerPruneReport, error) { + panic("implement me") +} diff --git a/pkg/domain/infra/tunnel/helpers.go b/pkg/domain/infra/tunnel/helpers.go new file mode 100644 index 000000000..d5a3224c2 --- /dev/null +++ b/pkg/domain/infra/tunnel/helpers.go @@ -0,0 +1,41 @@ +package tunnel + +import ( + "context" + + "github.com/containers/libpod/pkg/api/handlers/libpod" + "github.com/containers/libpod/pkg/bindings" + "github.com/containers/libpod/pkg/bindings/containers" + "github.com/containers/libpod/pkg/util" + "github.com/pkg/errors" +) + +func getContainersByContext(contextWithConnection context.Context, all bool, namesOrIds []string) ([]libpod.ListContainer, error) { + var ( + cons []libpod.ListContainer + ) + if all && len(namesOrIds) > 0 { + return nil, errors.New("cannot lookup containers and all") + } + c, err := containers.List(contextWithConnection, nil, &bindings.PTrue, nil, nil, nil, &bindings.PTrue) + if err != nil { + return nil, err + } + if all { + return c, err + } + for _, id := range namesOrIds { + var found bool + for _, con := range c { + if id == con.ID || util.StringInSlice(id, con.Names) { + cons = append(cons, con) + found = true + break + } + } + if !found { + return nil, errors.Errorf("unable to find container %q", id) + } + } + return cons, 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/runtime.go b/pkg/domain/infra/tunnel/runtime.go index af433a6d9..eb9b34e4a 100644 --- a/pkg/domain/infra/tunnel/runtime.go +++ b/pkg/domain/infra/tunnel/runtime.go @@ -16,18 +16,6 @@ type ContainerEngine struct { ClientCxt context.Context } -func (r *ContainerEngine) Shutdown(force bool) error { - return nil -} - -func (r *ContainerEngine) ContainerDelete(ctx context.Context, opts entities.ContainerDeleteOptions) (*entities.ContainerDeleteReport, error) { - panic("implement me") -} - -func (r *ContainerEngine) ContainerPrune(ctx context.Context) (*entities.ContainerPruneReport, error) { - panic("implement me") -} - func (r *ContainerEngine) PodDelete(ctx context.Context, opts entities.PodPruneOptions) (*entities.PodDeleteReport, error) { panic("implement me") } 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/config_unsupported.go b/pkg/specgen/config_unsupported.go index 5d24ac39c..c2d3257c9 100644 --- a/pkg/specgen/config_unsupported.go +++ b/pkg/specgen/config_unsupported.go @@ -3,10 +3,11 @@ package specgen import ( + "github.com/containers/libpod/libpod/image" spec "github.com/opencontainers/runtime-spec/specs-go" "github.com/pkg/errors" ) -func (s *SpecGenerator) getSeccompConfig(configSpec *spec.Spec) (*spec.LinuxSeccomp, error) { +func (s *SpecGenerator) getSeccompConfig(configSpec *spec.Spec, img *image.Image) (*spec.LinuxSeccomp, error) { return nil, errors.New("function not supported on non-linux OS's") } 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"` diff --git a/pkg/util/utils_linux_test.go b/pkg/util/utils_linux_test.go new file mode 100644 index 000000000..38e6dbef9 --- /dev/null +++ b/pkg/util/utils_linux_test.go @@ -0,0 +1,29 @@ +package util + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestGetImageConfigStopSignal(t *testing.T) { + // Linux-only beause parsing signal names is not supported on non-Linux systems by + // pkg/signal. + stopSignalValidInt, err := GetImageConfig([]string{"STOPSIGNAL 9"}) + require.Nil(t, err) + assert.Equal(t, stopSignalValidInt.StopSignal, "9") + + stopSignalValidString, err := GetImageConfig([]string{"STOPSIGNAL SIGKILL"}) + require.Nil(t, err) + assert.Equal(t, stopSignalValidString.StopSignal, "9") + + _, err = GetImageConfig([]string{"STOPSIGNAL 0"}) + assert.NotNil(t, err) + + _, err = GetImageConfig([]string{"STOPSIGNAL garbage"}) + assert.NotNil(t, err) + + _, err = GetImageConfig([]string{"STOPSIGNAL "}) + assert.NotNil(t, err) +} diff --git a/pkg/util/utils_test.go b/pkg/util/utils_test.go index f4b03599d..0995d1e20 100644 --- a/pkg/util/utils_test.go +++ b/pkg/util/utils_test.go @@ -219,25 +219,6 @@ func TestGetImageConfigLabel(t *testing.T) { assert.NotNil(t, err) } -func TestGetImageConfigStopSignal(t *testing.T) { - stopSignalValidInt, err := GetImageConfig([]string{"STOPSIGNAL 9"}) - require.Nil(t, err) - assert.Equal(t, stopSignalValidInt.StopSignal, "9") - - stopSignalValidString, err := GetImageConfig([]string{"STOPSIGNAL SIGKILL"}) - require.Nil(t, err) - assert.Equal(t, stopSignalValidString.StopSignal, "9") - - _, err = GetImageConfig([]string{"STOPSIGNAL 0"}) - assert.NotNil(t, err) - - _, err = GetImageConfig([]string{"STOPSIGNAL garbage"}) - assert.NotNil(t, err) - - _, err = GetImageConfig([]string{"STOPSIGNAL "}) - assert.NotNil(t, err) -} - func TestGetImageConfigOnBuild(t *testing.T) { onBuildOne, err := GetImageConfig([]string{"ONBUILD ADD /testdir1"}) require.Nil(t, err) |