diff options
Diffstat (limited to 'pkg')
49 files changed, 1868 insertions, 95 deletions
diff --git a/pkg/api/handlers/compat/images_build.go b/pkg/api/handlers/compat/images_build.go index 08646202a..1a24f1ae3 100644 --- a/pkg/api/handlers/compat/images_build.go +++ b/pkg/api/handlers/compat/images_build.go @@ -123,6 +123,7 @@ func BuildImage(w http.ResponseWriter, r *http.Request) { Tags []string `schema:"t"` Target string `schema:"target"` Timestamp int64 `schema:"timestamp"` + TLSVerify bool `schema:"tlsVerify"` Ulimits string `schema:"ulimits"` UnsetEnvs []string `schema:"unsetenv"` Secrets string `schema:"secrets"` @@ -491,6 +492,11 @@ func BuildImage(w http.ResponseWriter, r *http.Request) { } utils.PossiblyEnforceDockerHub(r, systemContext) + if _, found := r.URL.Query()["tlsVerify"]; found { + systemContext.DockerInsecureSkipTLSVerify = types.NewOptionalBool(!query.TLSVerify) + systemContext.OCIInsecureSkipTLSVerify = !query.TLSVerify + systemContext.DockerDaemonInsecureSkipTLSVerify = !query.TLSVerify + } // Channels all mux'ed in select{} below to follow API build protocol stdout := channel.NewWriter(make(chan []byte)) defer stdout.Close() diff --git a/pkg/autoupdate/autoupdate.go b/pkg/autoupdate/autoupdate.go index 07962a965..ee530528e 100644 --- a/pkg/autoupdate/autoupdate.go +++ b/pkg/autoupdate/autoupdate.go @@ -209,7 +209,7 @@ func autoUpdateRegistry(ctx context.Context, image *libimage.Image, ctr *libpod. } authfile := getAuthfilePath(ctr, options) - needsUpdate, err := newerRemoteImageAvailable(ctx, runtime, image, rawImageName, authfile) + needsUpdate, err := newerRemoteImageAvailable(ctx, image, rawImageName, authfile) if err != nil { return report, errors.Wrapf(err, "registry auto-updating container %q: image check for %q failed", cid, rawImageName) } @@ -399,7 +399,7 @@ func getAuthfilePath(ctr *libpod.Container, options *entities.AutoUpdateOptions) // newerRemoteImageAvailable returns true if there corresponding image on the remote // registry is newer. -func newerRemoteImageAvailable(ctx context.Context, runtime *libpod.Runtime, img *libimage.Image, origName string, authfile string) (bool, error) { +func newerRemoteImageAvailable(ctx context.Context, img *libimage.Image, origName string, authfile string) (bool, error) { remoteRef, err := docker.ParseReference("//" + origName) if err != nil { return false, err diff --git a/pkg/bindings/README.md b/pkg/bindings/README.md index 713adb104..ebc8a13d1 100644 --- a/pkg/bindings/README.md +++ b/pkg/bindings/README.md @@ -9,7 +9,7 @@ The bindings require that the Podman system service is running for the specified by calling the service directly. ### Starting the service with system -The command to start the Podman service differs slightly depending on the user that is running the service. For a rootfull service, +The command to start the Podman service differs slightly depending on the user that is running the service. For a rootful service, start the service like this: ``` # systemctl start podman.socket @@ -26,7 +26,7 @@ It can be handy to run the system service manually. Doing so allows you to enab $ podman --log-level=debug system service -t0 ``` If you do not provide a specific path for the socket, a default is provided. The location of that socket for -rootfull connections is `/run/podman/podman.sock` and for rootless it is `/run/USERID#/podman/podman.sock`. For more +rootful connections is `/run/podman/podman.sock` and for rootless it is `/run/USERID#/podman/podman.sock`. For more information about the Podman system service, see `man podman-system-service`. ### Creating a connection @@ -35,7 +35,7 @@ as they will be required to compile a Go program making use of the bindings. The first step for using the bindings is to create a connection to the socket. As mentioned earlier, the destination -of the socket depends on the user who owns it. In this case, a rootfull connection is made. +of the socket depends on the user who owns it. In this case, a rootful connection is made. ``` import ( @@ -59,7 +59,7 @@ The `conn` variable returned from the `bindings.NewConnection` function can then to interact with containers. ### Examples -The following examples build upon the connection example from above. They are all rootfull connections as well. +The following examples build upon the connection example from above. They are all rootful connections as well. Note: Optional arguments to the bindings methods are set using With*() methods on *Option structures. Composite types are not duplicated rather the address is used. As such, you should not change an underlying diff --git a/pkg/bindings/images/build.go b/pkg/bindings/images/build.go index 15900a2ed..1729bd922 100644 --- a/pkg/bindings/images/build.go +++ b/pkg/bindings/images/build.go @@ -312,10 +312,15 @@ func Build(ctx context.Context, containerFiles []string, options entities.BuildO var ( headers http.Header ) - if options.SystemContext != nil && options.SystemContext.DockerAuthConfig != nil { - headers, err = auth.MakeXRegistryAuthHeader(options.SystemContext, options.SystemContext.DockerAuthConfig.Username, options.SystemContext.DockerAuthConfig.Password) - } else { - headers, err = auth.MakeXRegistryConfigHeader(options.SystemContext, "", "") + if options.SystemContext != nil { + if options.SystemContext.DockerAuthConfig != nil { + headers, err = auth.MakeXRegistryAuthHeader(options.SystemContext, options.SystemContext.DockerAuthConfig.Username, options.SystemContext.DockerAuthConfig.Password) + } else { + headers, err = auth.MakeXRegistryConfigHeader(options.SystemContext, "", "") + } + if options.SystemContext.DockerInsecureSkipTLSVerify == types.OptionalBoolTrue { + params.Set("tlsVerify", "false") + } } if err != nil { return nil, err diff --git a/pkg/domain/infra/abi/images.go b/pkg/domain/infra/abi/images.go index 43440b594..74478b26d 100644 --- a/pkg/domain/infra/abi/images.go +++ b/pkg/domain/infra/abi/images.go @@ -367,7 +367,7 @@ func (ir *ImageEngine) Transfer(ctx context.Context, source entities.ImageScpOpt if rootless.IsRootless() && (len(dest.User) == 0 || dest.User == "root") { // if we are rootless and do not have a destination user we can just use sudo return transferRootless(source, dest, podman, parentFlags) } - return transferRootfull(source, dest, podman, parentFlags) + return transferRootful(source, dest, podman, parentFlags) } func (ir *ImageEngine) Tag(ctx context.Context, nameOrID string, tags []string, options entities.ImageTagOptions) error { @@ -785,8 +785,8 @@ func transferRootless(source entities.ImageScpOptions, dest entities.ImageScpOpt return cmdLoad.Run() } -// transferRootfull creates new podman processes using exec.Command and a new uid/gid alongside a cleared environment -func transferRootfull(source entities.ImageScpOptions, dest entities.ImageScpOptions, podman string, parentFlags []string) error { +// TransferRootful creates new podman processes using exec.Command and a new uid/gid alongside a cleared environment +func transferRootful(source entities.ImageScpOptions, dest entities.ImageScpOptions, podman string, parentFlags []string) error { basicCommand := []string{podman} basicCommand = append(basicCommand, parentFlags...) saveCommand := append(basicCommand, "save") diff --git a/pkg/domain/infra/abi/play.go b/pkg/domain/infra/abi/play.go index c3f6bb17d..1d347ed8c 100644 --- a/pkg/domain/infra/abi/play.go +++ b/pkg/domain/infra/abi/play.go @@ -114,7 +114,7 @@ func (ic *ContainerEngine) PlayKube(ctx context.Context, body io.Reader, options return nil, errors.Wrap(err, "unable to read YAML as Kube PersistentVolumeClaim") } - r, err := ic.playKubePVC(ctx, &pvcYAML, options) + r, err := ic.playKubePVC(ctx, &pvcYAML) if err != nil { return nil, err } @@ -592,7 +592,7 @@ func (ic *ContainerEngine) getImageAndLabelInfo(ctx context.Context, cwd string, } // playKubePVC creates a podman volume from a kube persistent volume claim. -func (ic *ContainerEngine) playKubePVC(ctx context.Context, pvcYAML *v1.PersistentVolumeClaim, options entities.PlayKubeOptions) (*entities.PlayKubeReport, error) { +func (ic *ContainerEngine) playKubePVC(ctx context.Context, pvcYAML *v1.PersistentVolumeClaim) (*entities.PlayKubeReport, error) { var report entities.PlayKubeReport opts := make(map[string]string) diff --git a/pkg/domain/infra/runtime_libpod.go b/pkg/domain/infra/runtime_libpod.go index 5fdc252e2..ac557e9de 100644 --- a/pkg/domain/infra/runtime_libpod.go +++ b/pkg/domain/infra/runtime_libpod.go @@ -209,6 +209,10 @@ func getRuntime(ctx context.Context, fs *flag.FlagSet, opts *engineOpts) (*libpo options = append(options, libpod.WithEventsLogger(cfg.Engine.EventsLogger)) } + if fs.Changed("volumepath") { + options = append(options, libpod.WithVolumePath(cfg.Engine.VolumePath)) + } + if fs.Changed("cgroup-manager") { options = append(options, libpod.WithCgroupManager(cfg.Engine.CgroupManager)) } else { diff --git a/pkg/machine/config.go b/pkg/machine/config.go index 505311264..1103933cd 100644 --- a/pkg/machine/config.go +++ b/pkg/machine/config.go @@ -28,7 +28,7 @@ type InitOptions struct { URI url.URL Username string ReExec bool - Rootfull bool + Rootful bool // The numerical userid of the user that called machine UID string } @@ -95,7 +95,7 @@ type ListResponse struct { } type SetOptions struct { - Rootfull bool + Rootful bool } type SSHOptions struct { diff --git a/pkg/machine/e2e/basic_test.go b/pkg/machine/e2e/basic_test.go new file mode 100644 index 000000000..f67fb4c67 --- /dev/null +++ b/pkg/machine/e2e/basic_test.go @@ -0,0 +1,50 @@ +package e2e + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + . "github.com/onsi/gomega/gexec" +) + +var _ = Describe("run basic podman commands", func() { + var ( + mb *machineTestBuilder + testDir string + ) + + BeforeEach(func() { + testDir, mb = setup() + }) + AfterEach(func() { + teardown(originalHomeDir, testDir, mb) + }) + + It("Basic ops", func() { + name := randomString(12) + i := new(initMachine) + session, err := mb.setName(name).setCmd(i.withImagePath(mb.imagePath).withNow()).run() + Expect(err).To(BeNil()) + Expect(session).To(Exit(0)) + + bm := basicMachine{} + imgs, err := mb.setCmd(bm.withPodmanCommand([]string{"images", "-q"})).run() + Expect(err).To(BeNil()) + Expect(imgs).To(Exit(0)) + Expect(len(imgs.outputToStringSlice())).To(Equal(0)) + + newImgs, err := mb.setCmd(bm.withPodmanCommand([]string{"pull", "quay.io/libpod/alpine_nginx"})).run() + Expect(err).To(BeNil()) + Expect(newImgs).To(Exit(0)) + Expect(len(newImgs.outputToStringSlice())).To(Equal(1)) + + runAlp, err := mb.setCmd(bm.withPodmanCommand([]string{"run", "quay.io/libpod/alpine_nginx", "cat", "/etc/os-release"})).run() + Expect(err).To(BeNil()) + Expect(runAlp).To(Exit(0)) + Expect(runAlp.outputToString()).To(ContainSubstring("Alpine Linux")) + + rmCon, err := mb.setCmd(bm.withPodmanCommand([]string{"rm", "-a"})).run() + Expect(err).To(BeNil()) + Expect(rmCon).To(Exit(0)) + }) + +}) diff --git a/pkg/machine/e2e/config.go b/pkg/machine/e2e/config.go new file mode 100644 index 000000000..7d75ca6bc --- /dev/null +++ b/pkg/machine/e2e/config.go @@ -0,0 +1,174 @@ +package e2e + +import ( + "encoding/json" + "fmt" + "math/rand" + "os" + "os/exec" + "path/filepath" + "strings" + "time" + + "github.com/containers/podman/v4/pkg/machine" + "github.com/containers/podman/v4/pkg/machine/qemu" + "github.com/containers/podman/v4/pkg/util" + . "github.com/onsi/ginkgo" //nolint:golint,stylecheck + . "github.com/onsi/gomega" + "github.com/onsi/gomega/gexec" + . "github.com/onsi/gomega/gexec" //nolint:golint,stylecheck +) + +var originalHomeDir = os.Getenv("HOME") + +const ( + defaultTimeout time.Duration = 90 * time.Second +) + +type machineCommand interface { + buildCmd(m *machineTestBuilder) []string +} + +type MachineTestBuilder interface { + setName(string) *MachineTestBuilder + setCmd(mc machineCommand) *MachineTestBuilder + setTimeout(duration time.Duration) *MachineTestBuilder + run() (*machineSession, error) +} +type machineSession struct { + *gexec.Session +} + +type machineTestBuilder struct { + cmd []string + imagePath string + name string + names []string + podmanBinary string + timeout time.Duration +} +type qemuMachineInspectInfo struct { + State machine.Status + VM qemu.MachineVM +} + +// waitWithTimeout waits for a command to complete for a given +// number of seconds +func (ms *machineSession) waitWithTimeout(timeout time.Duration) { + Eventually(ms, timeout).Should(Exit()) + os.Stdout.Sync() + os.Stderr.Sync() +} + +func (ms *machineSession) Bytes() []byte { + return []byte(ms.outputToString()) +} + +func (ms *machineSession) outputToStringSlice() []string { + var results []string + output := string(ms.Out.Contents()) + for _, line := range strings.Split(output, "\n") { + if line != "" { + results = append(results, line) + } + } + return results +} + +// outputToString returns the output from a session in string form +func (ms *machineSession) outputToString() string { + if ms == nil || ms.Out == nil || ms.Out.Contents() == nil { + return "" + } + + fields := strings.Fields(string(ms.Out.Contents())) + return strings.Join(fields, " ") +} + +// newMB constructor for machine test builders +func newMB() (*machineTestBuilder, error) { + mb := machineTestBuilder{ + timeout: defaultTimeout, + } + cwd, err := os.Getwd() + if err != nil { + return nil, err + } + mb.podmanBinary = filepath.Join(cwd, "../../../bin/podman-remote") + if os.Getenv("PODMAN_BINARY") != "" { + mb.podmanBinary = os.Getenv("PODMAN_BINARY") + } + return &mb, nil +} + +// setName sets the name of the virtuaql machine for the command +func (m *machineTestBuilder) setName(name string) *machineTestBuilder { + m.name = name + return m +} + +// setCmd takes a machineCommand struct and assembles a cmd line +// representation of the podman machine command +func (m *machineTestBuilder) setCmd(mc machineCommand) *machineTestBuilder { + // If no name for the machine exists, we set a random name. + if !util.StringInSlice(m.name, m.names) { + if len(m.name) < 1 { + m.name = randomString(12) + } + m.names = append(m.names, m.name) + } + m.cmd = mc.buildCmd(m) + return m +} + +func (m *machineTestBuilder) setTimeout(timeout time.Duration) *machineTestBuilder { + m.timeout = timeout + return m +} + +// toQemuInspectInfo is only for inspecting qemu machines. Other providers will need +// to make their own. +func (mb *machineTestBuilder) toQemuInspectInfo() ([]qemuMachineInspectInfo, int, error) { + args := []string{"machine", "inspect"} + args = append(args, mb.names...) + session, err := runWrapper(mb.podmanBinary, args, defaultTimeout) + if err != nil { + return nil, -1, err + } + mii := []qemuMachineInspectInfo{} + err = json.Unmarshal(session.Bytes(), &mii) + return mii, session.ExitCode(), err +} + +func (m *machineTestBuilder) run() (*machineSession, error) { + return runWrapper(m.podmanBinary, m.cmd, m.timeout) +} + +func runWrapper(podmanBinary string, cmdArgs []string, timeout time.Duration) (*machineSession, error) { + if len(os.Getenv("DEBUG")) > 0 { + cmdArgs = append([]string{"--log-level=debug"}, cmdArgs...) + } + fmt.Println(podmanBinary + " " + strings.Join(cmdArgs, " ")) + c := exec.Command(podmanBinary, cmdArgs...) + session, err := Start(c, GinkgoWriter, GinkgoWriter) + if err != nil { + Fail(fmt.Sprintf("Unable to start session: %q", err)) + return nil, err + } + ms := machineSession{session} + ms.waitWithTimeout(timeout) + fmt.Println("output:", ms.outputToString()) + return &ms, nil +} + +func (m *machineTestBuilder) init() {} + +// randomString returns a string of given length composed of random characters +func randomString(n int) string { + var randomLetters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") + b := make([]rune, n) + for i := range b { + b[i] = randomLetters[rand.Intn(len(randomLetters))] + } + return string(b) +} diff --git a/pkg/machine/e2e/config_basic.go b/pkg/machine/e2e/config_basic.go new file mode 100644 index 000000000..be0896156 --- /dev/null +++ b/pkg/machine/e2e/config_basic.go @@ -0,0 +1,19 @@ +package e2e + +type basicMachine struct { + args []string + cmd []string +} + +func (s basicMachine) buildCmd(m *machineTestBuilder) []string { + cmd := []string{"-r"} + if len(s.args) > 0 { + cmd = append(cmd, s.args...) + } + return cmd +} + +func (s *basicMachine) withPodmanCommand(args []string) *basicMachine { + s.args = args + return s +} diff --git a/pkg/machine/e2e/config_init.go b/pkg/machine/e2e/config_init.go new file mode 100644 index 000000000..55218221d --- /dev/null +++ b/pkg/machine/e2e/config_init.go @@ -0,0 +1,101 @@ +package e2e + +import ( + "strconv" +) + +type initMachine struct { + /* + --cpus uint Number of CPUs (default 1) + --disk-size uint Disk size in GB (default 100) + --ignition-path string Path to ignition file + --image-path string Path to qcow image (default "testing") + -m, --memory uint Memory in MB (default 2048) + --now Start machine now + --rootful Whether this machine should prefer rootful container exectution + --timezone string Set timezone (default "local") + -v, --volume stringArray Volumes to mount, source:target + --volume-driver string Optional volume driver + + */ + cpus *uint + diskSize *uint + ignitionPath string + imagePath string + memory *uint + now bool + timezone string + volumes []string + + cmd []string +} + +func (i *initMachine) buildCmd(m *machineTestBuilder) []string { + cmd := []string{"machine", "init"} + if i.cpus != nil { + cmd = append(cmd, "--cpus", strconv.Itoa(int(*i.cpus))) + } + if i.diskSize != nil { + cmd = append(cmd, "--disk-size", strconv.Itoa(int(*i.diskSize))) + } + if l := len(i.ignitionPath); l > 0 { + cmd = append(cmd, "--ignition-path", i.ignitionPath) + } + if l := len(i.imagePath); l > 0 { + cmd = append(cmd, "--image-path", i.imagePath) + } + if i.memory != nil { + cmd = append(cmd, "--memory", strconv.Itoa(int(*i.memory))) + } + if l := len(i.timezone); l > 0 { + cmd = append(cmd, "--timezone", i.timezone) + } + for _, v := range i.volumes { + cmd = append(cmd, "--volume", v) + } + if i.now { + cmd = append(cmd, "--now") + } + cmd = append(cmd, m.name) + i.cmd = cmd + return cmd +} + +func (i *initMachine) withCPUs(num uint) *initMachine { + i.cpus = &num + return i +} +func (i *initMachine) withDiskSize(size uint) *initMachine { + i.diskSize = &size + return i +} + +func (i *initMachine) withIgnitionPath(path string) *initMachine { + i.ignitionPath = path + return i +} + +func (i *initMachine) withImagePath(path string) *initMachine { + i.imagePath = path + return i +} + +func (i *initMachine) withMemory(num uint) *initMachine { + i.memory = &num + return i +} + +func (i *initMachine) withNow() *initMachine { + i.now = true + return i +} + +func (i *initMachine) withTimezone(tz string) *initMachine { + i.timezone = tz + return i +} + +func (i *initMachine) withVolume(v string) *initMachine { + i.volumes = append(i.volumes, v) + return i +} diff --git a/pkg/machine/e2e/config_inspect.go b/pkg/machine/e2e/config_inspect.go new file mode 100644 index 000000000..74c9a5d9c --- /dev/null +++ b/pkg/machine/e2e/config_inspect.go @@ -0,0 +1,24 @@ +package e2e + +type inspectMachine struct { + /* + --format string Format volume output using JSON or a Go template + */ + cmd []string + format string +} + +func (i *inspectMachine) buildCmd(m *machineTestBuilder) []string { + cmd := []string{"machine", "inspect"} + if len(i.format) > 0 { + cmd = append(cmd, "--format", i.format) + } + cmd = append(cmd, m.names...) + i.cmd = cmd + return cmd +} + +func (i *inspectMachine) withFormat(format string) *inspectMachine { + i.format = format + return i +} diff --git a/pkg/machine/e2e/config_list.go b/pkg/machine/e2e/config_list.go new file mode 100644 index 000000000..150f984bc --- /dev/null +++ b/pkg/machine/e2e/config_list.go @@ -0,0 +1,45 @@ +package e2e + +type listMachine struct { + /* + --format string Format volume output using JSON or a Go template (default "{{.Name}}\t{{.VMType}}\t{{.Created}}\t{{.LastUp}}\t{{.CPUs}}\t{{.Memory}}\t{{.DiskSize}}\n") + --noheading Do not print headers + -q, --quiet Show only machine names + */ + + format string + noHeading bool + quiet bool + + cmd []string +} + +func (i *listMachine) buildCmd(m *machineTestBuilder) []string { + cmd := []string{"machine", "list"} + if len(i.format) > 0 { + cmd = append(cmd, "--format", i.format) + } + if i.noHeading { + cmd = append(cmd, "--noheading") + } + if i.quiet { + cmd = append(cmd, "--quiet") + } + i.cmd = cmd + return cmd +} + +func (i *listMachine) withNoHeading() *listMachine { + i.noHeading = true + return i +} + +func (i *listMachine) withQuiet() *listMachine { + i.quiet = true + return i +} + +func (i *listMachine) withFormat(format string) *listMachine { + i.format = format + return i +} diff --git a/pkg/machine/e2e/config_rm.go b/pkg/machine/e2e/config_rm.go new file mode 100644 index 000000000..6cf262a22 --- /dev/null +++ b/pkg/machine/e2e/config_rm.go @@ -0,0 +1,56 @@ +package e2e + +type rmMachine struct { + /* + -f, --force Stop and do not prompt before rming + --save-ignition Do not delete ignition file + --save-image Do not delete the image file + --save-keys Do not delete SSH keys + + */ + force bool + saveIgnition bool + saveImage bool + saveKeys bool + + cmd []string +} + +func (i *rmMachine) buildCmd(m *machineTestBuilder) []string { + cmd := []string{"machine", "rm"} + if i.force { + cmd = append(cmd, "--force") + } + if i.saveIgnition { + cmd = append(cmd, "--save-ignition") + } + if i.saveImage { + cmd = append(cmd, "--save-image") + } + if i.saveKeys { + cmd = append(cmd, "--save-keys") + } + cmd = append(cmd, m.name) + i.cmd = cmd + return cmd +} + +func (i *rmMachine) withForce() *rmMachine { + i.force = true + return i +} + +func (i *rmMachine) withSaveIgnition() *rmMachine { + i.saveIgnition = true + return i +} + +func (i *rmMachine) withSaveImage() *rmMachine { + i.saveImage = true + return i +} + +func (i *rmMachine) withSaveKeys() *rmMachine { + i.saveKeys = true + return i +} diff --git a/pkg/machine/e2e/config_ssh.go b/pkg/machine/e2e/config_ssh.go new file mode 100644 index 000000000..b09eed47d --- /dev/null +++ b/pkg/machine/e2e/config_ssh.go @@ -0,0 +1,33 @@ +package e2e + +type sshMachine struct { + /* + --username string Username to use when ssh-ing into the VM. + */ + + username string + sshCommand []string + + cmd []string +} + +func (s sshMachine) buildCmd(m *machineTestBuilder) []string { + cmd := []string{"machine", "ssh"} + if len(m.name) > 0 { + cmd = append(cmd, m.name) + } + if len(s.sshCommand) > 0 { + cmd = append(cmd, s.sshCommand...) + } + return cmd +} + +func (s *sshMachine) withUsername(name string) *sshMachine { + s.username = name + return s +} + +func (s *sshMachine) withSSHComand(sshCommand []string) *sshMachine { + s.sshCommand = sshCommand + return s +} diff --git a/pkg/machine/e2e/config_start.go b/pkg/machine/e2e/config_start.go new file mode 100644 index 000000000..86b1721f8 --- /dev/null +++ b/pkg/machine/e2e/config_start.go @@ -0,0 +1,16 @@ +package e2e + +type startMachine struct { + /* + No command line args other than a machine vm name (also not required) + */ + cmd []string +} + +func (s startMachine) buildCmd(m *machineTestBuilder) []string { + cmd := []string{"machine", "start"} + if len(m.name) > 0 { + cmd = append(cmd, m.name) + } + return cmd +} diff --git a/pkg/machine/e2e/config_stop.go b/pkg/machine/e2e/config_stop.go new file mode 100644 index 000000000..04dcfb524 --- /dev/null +++ b/pkg/machine/e2e/config_stop.go @@ -0,0 +1,16 @@ +package e2e + +type stopMachine struct { + /* + No command line args other than a machine vm name (also not required) + */ + cmd []string +} + +func (s stopMachine) buildCmd(m *machineTestBuilder) []string { + cmd := []string{"machine", "stop"} + if len(m.name) > 0 { + cmd = append(cmd, m.name) + } + return cmd +} diff --git a/pkg/machine/e2e/init_test.go b/pkg/machine/e2e/init_test.go new file mode 100644 index 000000000..309d460a9 --- /dev/null +++ b/pkg/machine/e2e/init_test.go @@ -0,0 +1,77 @@ +package e2e + +import ( + "time" + + "github.com/containers/podman/v4/pkg/machine" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + . "github.com/onsi/gomega/gexec" +) + +var _ = Describe("podman machine init", func() { + var ( + mb *machineTestBuilder + testDir string + ) + + BeforeEach(func() { + testDir, mb = setup() + }) + AfterEach(func() { + teardown(originalHomeDir, testDir, mb) + }) + + It("bad init name", func() { + i := initMachine{} + reallyLongName := "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" + session, err := mb.setName(reallyLongName).setCmd(&i).run() + Expect(err).To(BeNil()) + Expect(session.ExitCode()).To(Equal(125)) + }) + It("simple init", func() { + i := new(initMachine) + session, err := mb.setCmd(i.withImagePath(mb.imagePath)).run() + Expect(err).To(BeNil()) + Expect(session.ExitCode()).To(Equal(0)) + + inspectBefore, ec, err := mb.toQemuInspectInfo() + Expect(err).To(BeNil()) + Expect(ec).To(BeZero()) + + Expect(len(inspectBefore)).To(BeNumerically(">", 0)) + testMachine := inspectBefore[0] + Expect(testMachine.VM.Name).To(Equal(mb.names[0])) + Expect(testMachine.VM.CPUs).To(Equal(uint64(1))) + Expect(testMachine.VM.Memory).To(Equal(uint64(2048))) + + }) + + It("simple init with start", func() { + i := initMachine{} + session, err := mb.setCmd(i.withImagePath(mb.imagePath)).run() + Expect(err).To(BeNil()) + Expect(session.ExitCode()).To(Equal(0)) + + inspectBefore, ec, err := mb.toQemuInspectInfo() + Expect(ec).To(BeZero()) + Expect(len(inspectBefore)).To(BeNumerically(">", 0)) + Expect(err).To(BeNil()) + Expect(len(inspectBefore)).To(BeNumerically(">", 0)) + Expect(inspectBefore[0].VM.Name).To(Equal(mb.names[0])) + + s := startMachine{} + ssession, err := mb.setCmd(s).setTimeout(time.Minute * 10).run() + Expect(err).To(BeNil()) + Expect(ssession).Should(Exit(0)) + + inspectAfter, ec, err := mb.toQemuInspectInfo() + Expect(err).To(BeNil()) + Expect(ec).To(BeZero()) + Expect(len(inspectBefore)).To(BeNumerically(">", 0)) + Expect(len(inspectAfter)).To(BeNumerically(">", 0)) + Expect(inspectAfter[0].State).To(Equal(machine.Running)) + }) + +}) diff --git a/pkg/machine/e2e/inspect_test.go b/pkg/machine/e2e/inspect_test.go new file mode 100644 index 000000000..30d810b8f --- /dev/null +++ b/pkg/machine/e2e/inspect_test.go @@ -0,0 +1,67 @@ +package e2e + +import ( + "encoding/json" + + "github.com/containers/podman/v4/pkg/machine/qemu" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("podman machine stop", func() { + var ( + mb *machineTestBuilder + testDir string + ) + + BeforeEach(func() { + testDir, mb = setup() + }) + AfterEach(func() { + teardown(originalHomeDir, testDir, mb) + }) + + It("inspect bad name", func() { + i := inspectMachine{} + reallyLongName := "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" + session, err := mb.setName(reallyLongName).setCmd(&i).run() + Expect(err).To(BeNil()) + Expect(session.ExitCode()).To(Equal(125)) + }) + + It("inspect two machines", func() { + i := new(initMachine) + foo1, err := mb.setName("foo1").setCmd(i.withImagePath(mb.imagePath)).run() + Expect(err).To(BeNil()) + Expect(foo1.ExitCode()).To(Equal(0)) + + ii := new(initMachine) + foo2, err := mb.setName("foo2").setCmd(ii.withImagePath(mb.imagePath)).run() + Expect(err).To(BeNil()) + Expect(foo2.ExitCode()).To(Equal(0)) + + inspect := new(inspectMachine) + inspectSession, err := mb.setName("foo1").setCmd(inspect).run() + Expect(err).To(BeNil()) + Expect(inspectSession.ExitCode()).To(Equal(0)) + + type fakeInfos struct { + Status string + VM qemu.MachineVM + } + infos := make([]fakeInfos, 0, 2) + err = json.Unmarshal(inspectSession.Bytes(), &infos) + Expect(err).ToNot(HaveOccurred()) + Expect(len(infos)).To(Equal(2)) + + //rm := new(rmMachine) + //// Must manually clean up due to multiple names + //for _, name := range []string{"foo1", "foo2"} { + // mb.setName(name).setCmd(rm.withForce()).run() + // mb.names = []string{} + //} + //mb.names = []string{} + + }) +}) diff --git a/pkg/machine/e2e/list_test.go b/pkg/machine/e2e/list_test.go new file mode 100644 index 000000000..e7a439945 --- /dev/null +++ b/pkg/machine/e2e/list_test.go @@ -0,0 +1,79 @@ +package e2e + +import ( + "strings" + + "github.com/containers/buildah/util" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + . "github.com/onsi/gomega/gexec" +) + +var _ = Describe("podman machine list", func() { + var ( + mb *machineTestBuilder + testDir string + ) + + BeforeEach(func() { + testDir, mb = setup() + }) + AfterEach(func() { + teardown(originalHomeDir, testDir, mb) + }) + + It("list machine", func() { + list := new(listMachine) + firstList, err := mb.setCmd(list).run() + Expect(err).NotTo(HaveOccurred()) + Expect(firstList).Should(Exit(0)) + Expect(len(firstList.outputToStringSlice())).To(Equal(1)) // just the header + + i := new(initMachine) + session, err := mb.setCmd(i.withImagePath(mb.imagePath)).run() + Expect(err).To(BeNil()) + Expect(session).To(Exit(0)) + + secondList, err := mb.setCmd(list).run() + Expect(err).NotTo(HaveOccurred()) + Expect(secondList).To(Exit(0)) + Expect(len(secondList.outputToStringSlice())).To(Equal(2)) // one machine and the header + }) + + It("list machines with quiet", func() { + // Random names for machines to test list + name1 := randomString(12) + name2 := randomString(12) + + list := new(listMachine) + firstList, err := mb.setCmd(list.withQuiet()).run() + Expect(err).NotTo(HaveOccurred()) + Expect(firstList).Should(Exit(0)) + Expect(len(firstList.outputToStringSlice())).To(Equal(0)) // No header with quiet + + i := new(initMachine) + session, err := mb.setName(name1).setCmd(i.withImagePath(mb.imagePath)).run() + Expect(err).To(BeNil()) + Expect(session).To(Exit(0)) + + session2, err := mb.setName(name2).setCmd(i.withImagePath(mb.imagePath)).run() + Expect(err).To(BeNil()) + Expect(session2).To(Exit(0)) + + secondList, err := mb.setCmd(list.withQuiet()).run() + Expect(err).NotTo(HaveOccurred()) + Expect(secondList).To(Exit(0)) + Expect(len(secondList.outputToStringSlice())).To(Equal(2)) // two machines, no header + + listNames := secondList.outputToStringSlice() + stripAsterisk(listNames) + Expect(util.StringInSlice(name1, listNames)).To(BeTrue()) + Expect(util.StringInSlice(name2, listNames)).To(BeTrue()) + }) +}) + +func stripAsterisk(sl []string) { + for idx, val := range sl { + sl[idx] = strings.TrimRight(val, "*") + } +} diff --git a/pkg/machine/e2e/machine_test.go b/pkg/machine/e2e/machine_test.go new file mode 100644 index 000000000..46fe18069 --- /dev/null +++ b/pkg/machine/e2e/machine_test.go @@ -0,0 +1,129 @@ +package e2e + +import ( + "fmt" + "io" + "io/ioutil" + url2 "net/url" + "os" + "path" + "path/filepath" + "strings" + "testing" + "time" + + "github.com/containers/podman/v4/pkg/machine" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +func TestMain(m *testing.M) { + os.Exit(m.Run()) +} + +const ( + defaultStream string = "podman-testing" + tmpDir string = "/var/tmp" +) + +var ( + fqImageName string + suiteImageName string +) + +// TestLibpod ginkgo master function +func TestMachine(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Podman Machine tests") +} + +var _ = BeforeSuite(func() { + fcd, err := machine.GetFCOSDownload(defaultStream) + if err != nil { + Fail("unable to get virtual machine image") + } + suiteImageName = strings.TrimSuffix(path.Base(fcd.Location), ".xz") + fqImageName = filepath.Join(tmpDir, suiteImageName) + if _, err := os.Stat(fqImageName); err != nil { + if os.IsNotExist(err) { + getMe, err := url2.Parse(fcd.Location) + if err != nil { + Fail(fmt.Sprintf("unable to create url for download: %q", err)) + } + now := time.Now() + if err := machine.DownloadVMImage(getMe, fqImageName+".xz"); err != nil { + Fail(fmt.Sprintf("unable to download machine image: %q", err)) + } + fmt.Println("Download took: ", time.Since(now).String()) + if err := machine.Decompress(fqImageName+".xz", fqImageName); err != nil { + Fail(fmt.Sprintf("unable to decompress image file: %q", err)) + } + } else { + Fail(fmt.Sprintf("unable to check for cache image: %q", err)) + } + } +}) + +var _ = SynchronizedAfterSuite(func() {}, + func() { + fmt.Println("After") + }) + +func setup() (string, *machineTestBuilder) { + homeDir, err := ioutil.TempDir("/var/tmp", "podman_test") + if err != nil { + Fail(fmt.Sprintf("failed to create home directory: %q", err)) + } + if err := os.MkdirAll(filepath.Join(homeDir, ".ssh"), 0700); err != nil { + Fail(fmt.Sprintf("failed to create ssh dir: %q", err)) + } + sshConfig, err := os.Create(filepath.Join(homeDir, ".ssh", "config")) + if err != nil { + Fail(fmt.Sprintf("failed to create ssh config: %q", err)) + } + if _, err := sshConfig.WriteString("IdentitiesOnly=yes"); err != nil { + Fail(fmt.Sprintf("failed to write ssh config: %q", err)) + } + if err := sshConfig.Close(); err != nil { + Fail(fmt.Sprintf("unable to close ssh config file descriptor: %q", err)) + } + if err := os.Setenv("HOME", homeDir); err != nil { + Fail("failed to set home dir") + } + if err := os.Unsetenv("SSH_AUTH_SOCK"); err != nil { + Fail("unable to unset SSH_AUTH_SOCK") + } + mb, err := newMB() + if err != nil { + Fail(fmt.Sprintf("failed to create machine test: %q", err)) + } + f, err := os.Open(fqImageName) + if err != nil { + Fail(fmt.Sprintf("failed to open file %s: %q", fqImageName, err)) + } + mb.imagePath = filepath.Join(homeDir, suiteImageName) + n, err := os.Create(mb.imagePath) + if err != nil { + Fail(fmt.Sprintf("failed to create file %s: %q", mb.imagePath, err)) + } + if _, err := io.Copy(n, f); err != nil { + Fail(fmt.Sprintf("failed to copy %ss to %s: %q", fqImageName, mb.imagePath, err)) + } + return homeDir, mb +} + +func teardown(origHomeDir string, testDir string, mb *machineTestBuilder) { + s := new(stopMachine) + for _, name := range mb.names { + if _, err := mb.setName(name).setCmd(s).run(); err != nil { + fmt.Printf("error occured rm'ing machine: %q\n", err) + } + } + if err := os.RemoveAll(testDir); err != nil { + Fail(fmt.Sprintf("failed to remove test dir: %q", err)) + } + // this needs to be last in teardown + if err := os.Setenv("HOME", origHomeDir); err != nil { + Fail("failed to set home dir") + } +} diff --git a/pkg/machine/e2e/rm_test.go b/pkg/machine/e2e/rm_test.go new file mode 100644 index 000000000..011da5dde --- /dev/null +++ b/pkg/machine/e2e/rm_test.go @@ -0,0 +1,67 @@ +package e2e + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("podman machine rm", func() { + var ( + mb *machineTestBuilder + testDir string + ) + + BeforeEach(func() { + testDir, mb = setup() + }) + AfterEach(func() { + teardown(originalHomeDir, testDir, mb) + }) + + It("bad init name", func() { + i := rmMachine{} + reallyLongName := "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" + session, err := mb.setName(reallyLongName).setCmd(&i).run() + Expect(err).To(BeNil()) + Expect(session.ExitCode()).To(Equal(125)) + }) + + It("Remove machine", func() { + i := new(initMachine) + session, err := mb.setCmd(i.withImagePath(mb.imagePath)).run() + Expect(err).To(BeNil()) + Expect(session.ExitCode()).To(Equal(0)) + rm := rmMachine{} + _, err = mb.setCmd(rm.withForce()).run() + Expect(err).To(BeNil()) + + // Inspecting a non-existent machine should fail + // which means it is gone + _, ec, err := mb.toQemuInspectInfo() + Expect(err).To(BeNil()) + Expect(ec).To(Equal(125)) + }) + + It("Remove running machine", func() { + i := new(initMachine) + session, err := mb.setCmd(i.withImagePath(mb.imagePath).withNow()).run() + Expect(err).To(BeNil()) + Expect(session.ExitCode()).To(Equal(0)) + rm := new(rmMachine) + + // Removing a running machine should fail + stop, err := mb.setCmd(rm).run() + Expect(err).To(BeNil()) + Expect(stop.ExitCode()).To(Equal(125)) + + // Removing again with force + stopAgain, err := mb.setCmd(rm.withForce()).run() + Expect(err).To(BeNil()) + Expect(stopAgain.ExitCode()).To(BeZero()) + + // Inspect to be dead sure + _, ec, err := mb.toQemuInspectInfo() + Expect(err).To(BeNil()) + Expect(ec).To(Equal(125)) + }) +}) diff --git a/pkg/machine/e2e/ssh_test.go b/pkg/machine/e2e/ssh_test.go new file mode 100644 index 000000000..90296fa10 --- /dev/null +++ b/pkg/machine/e2e/ssh_test.go @@ -0,0 +1,59 @@ +package e2e + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("podman machine ssh", func() { + var ( + mb *machineTestBuilder + testDir string + ) + + BeforeEach(func() { + testDir, mb = setup() + }) + AfterEach(func() { + teardown(originalHomeDir, testDir, mb) + }) + + It("bad machine name", func() { + name := randomString(12) + ssh := sshMachine{} + session, err := mb.setName(name).setCmd(ssh).run() + Expect(err).To(BeNil()) + Expect(session.ExitCode()).To(Equal(125)) + // TODO seems like stderr is not being returned; re-enabled when fixed + //Expect(session.outputToString()).To(ContainSubstring("not exist")) + }) + + It("ssh to non-running machine", func() { + name := randomString(12) + i := new(initMachine) + session, err := mb.setName(name).setCmd(i.withImagePath(mb.imagePath)).run() + Expect(err).To(BeNil()) + Expect(session.ExitCode()).To(Equal(0)) + + ssh := sshMachine{} + sshSession, err := mb.setName(name).setCmd(ssh).run() + Expect(err).To(BeNil()) + // TODO seems like stderr is not being returned; re-enabled when fixed + //Expect(sshSession.outputToString()).To(ContainSubstring("is not running")) + Expect(sshSession.ExitCode()).To(Equal(125)) + }) + + It("ssh to running machine and check os-type", func() { + name := randomString(12) + i := new(initMachine) + session, err := mb.setName(name).setCmd(i.withImagePath(mb.imagePath).withNow()).run() + Expect(err).To(BeNil()) + Expect(session.ExitCode()).To(Equal(0)) + + ssh := sshMachine{} + sshSession, err := mb.setName(name).setCmd(ssh.withSSHComand([]string{"cat", "/etc/os-release"})).run() + Expect(err).To(BeNil()) + Expect(sshSession.ExitCode()).To(Equal(0)) + Expect(sshSession.outputToString()).To(ContainSubstring("Fedora CoreOS")) + }) +}) diff --git a/pkg/machine/e2e/start_test.go b/pkg/machine/e2e/start_test.go new file mode 100644 index 000000000..1cda0e8f1 --- /dev/null +++ b/pkg/machine/e2e/start_test.go @@ -0,0 +1,36 @@ +package e2e + +import ( + "github.com/containers/podman/v4/pkg/machine" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("podman machine start", func() { + var ( + mb *machineTestBuilder + testDir string + ) + BeforeEach(func() { + testDir, mb = setup() + }) + AfterEach(func() { + teardown(originalHomeDir, testDir, mb) + }) + + It("start simple machine", func() { + i := new(initMachine) + session, err := mb.setCmd(i.withImagePath(mb.imagePath)).run() + Expect(err).To(BeNil()) + Expect(session.ExitCode()).To(Equal(0)) + s := new(startMachine) + startSession, err := mb.setCmd(s).run() + Expect(err).To(BeNil()) + Expect(startSession.ExitCode()).To(Equal(0)) + + info, ec, err := mb.toQemuInspectInfo() + Expect(err).To(BeNil()) + Expect(ec).To(BeZero()) + Expect(info[0].State).To(Equal(machine.Running)) + }) +}) diff --git a/pkg/machine/e2e/stop_test.go b/pkg/machine/e2e/stop_test.go new file mode 100644 index 000000000..5dee6a345 --- /dev/null +++ b/pkg/machine/e2e/stop_test.go @@ -0,0 +1,46 @@ +package e2e + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("podman machine stop", func() { + var ( + mb *machineTestBuilder + testDir string + ) + + BeforeEach(func() { + testDir, mb = setup() + }) + AfterEach(func() { + teardown(originalHomeDir, testDir, mb) + }) + + It("stop bad name", func() { + i := stopMachine{} + reallyLongName := "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" + session, err := mb.setName(reallyLongName).setCmd(&i).run() + Expect(err).To(BeNil()) + Expect(session.ExitCode()).To(Equal(125)) + }) + + It("Stop running machine", func() { + i := new(initMachine) + session, err := mb.setCmd(i.withImagePath(mb.imagePath).withNow()).run() + Expect(err).To(BeNil()) + Expect(session.ExitCode()).To(Equal(0)) + + stop := new(stopMachine) + // Removing a running machine should fail + stopSession, err := mb.setCmd(stop).run() + Expect(err).To(BeNil()) + Expect(stopSession.ExitCode()).To(Equal(0)) + + // Stopping it again should not result in an error + stopAgain, err := mb.setCmd(stop).run() + Expect(err).To(BeNil()) + Expect(stopAgain.ExitCode()).To(BeZero()) + }) +}) diff --git a/pkg/machine/fcos.go b/pkg/machine/fcos.go index d8516dd59..df58b8a1e 100644 --- a/pkg/machine/fcos.go +++ b/pkg/machine/fcos.go @@ -43,7 +43,7 @@ type FcosDownload struct { } func NewFcosDownloader(vmType, vmName, imageStream string) (DistributionDownload, error) { - info, err := getFCOSDownload(imageStream) + info, err := GetFCOSDownload(imageStream) if err != nil { return nil, err } @@ -79,7 +79,7 @@ func (f FcosDownload) Get() *Download { return &f.Download } -type fcosDownloadInfo struct { +type FcosDownloadInfo struct { CompressionType string Location string Release string @@ -139,7 +139,7 @@ func getStreamURL(streamType string) url2.URL { // This should get Exported and stay put as it will apply to all fcos downloads // getFCOS parses fedoraCoreOS's stream and returns the image download URL and the release version -func getFCOSDownload(imageStream string) (*fcosDownloadInfo, error) { // nolint:staticcheck +func GetFCOSDownload(imageStream string) (*FcosDownloadInfo, error) { //nolint:staticcheck var ( fcosstable stream.Stream altMeta release.Release @@ -150,8 +150,8 @@ func getFCOSDownload(imageStream string) (*fcosDownloadInfo, error) { // nolint: // fcos trees, we should remove it and re-release at least on // macs. // TODO: remove when podman4.0 is in coreos - // nolint:staticcheck - imageStream = "podman-testing" + + imageStream = "podman-testing" //nolint:staticcheck switch imageStream { case "podman-testing": @@ -194,7 +194,7 @@ func getFCOSDownload(imageStream string) (*fcosDownloadInfo, error) { // nolint: } disk := qcow2.Disk - return &fcosDownloadInfo{ + return &FcosDownloadInfo{ Location: disk.Location, Sha256Sum: disk.Sha256, CompressionType: "xz", @@ -228,7 +228,7 @@ func getFCOSDownload(imageStream string) (*fcosDownloadInfo, error) { // nolint: if disk == nil { return nil, fmt.Errorf("unable to pull VM image: no disk in stream") } - return &fcosDownloadInfo{ + return &FcosDownloadInfo{ Location: disk.Location, Release: qemu.Release, Sha256Sum: disk.Sha256, diff --git a/pkg/machine/ignition.go b/pkg/machine/ignition.go index fe47437e3..35a9a30cb 100644 --- a/pkg/machine/ignition.go +++ b/pkg/machine/ignition.go @@ -304,6 +304,8 @@ ExecStart=/usr/bin/sleep infinity containers := `[containers] netns="bridge" ` + // Set deprecated machine_enabled until podman package on fcos is + // current enough to no longer require it rootContainers := `[engine] machine_enabled=true ` @@ -392,7 +394,7 @@ Delegate=memory pids cpu io FileEmbedded1: FileEmbedded1{Mode: intToPtr(0644)}, }) - // Set machine_enabled to true to indicate we're in a VM + // Set deprecated machine_enabled to true to indicate we're in a VM files = append(files, File{ Node: Node{ Group: getNodeGrp("root"), @@ -408,6 +410,22 @@ Delegate=memory pids cpu io }, }) + // Set machine marker file to indicate podman is in a qemu based machine + files = append(files, File{ + Node: Node{ + Group: getNodeGrp("root"), + Path: "/etc/containers/podman-machine", + User: getNodeUsr("root"), + }, + FileEmbedded1: FileEmbedded1{ + Append: nil, + Contents: Resource{ + Source: encodeDataURLPtr("qemu\n"), + }, + Mode: intToPtr(0644), + }, + }) + // Issue #11489: make sure that we can inject a custom registries.conf // file on the system level to force a single search registry. // The remote client does not yet support prompting for short-name diff --git a/pkg/machine/qemu/config.go b/pkg/machine/qemu/config.go index 840bd5c59..9473eef6f 100644 --- a/pkg/machine/qemu/config.go +++ b/pkg/machine/qemu/config.go @@ -57,8 +57,8 @@ type MachineVMV1 struct { QMPMonitor Monitorv1 // RemoteUsername of the vm user RemoteUsername string - // Whether this machine should run in a rootfull or rootless manner - Rootfull bool + // Whether this machine should run in a rootful or rootless manner + Rootful bool // UID is the numerical id of the user that called machine UID int } @@ -105,8 +105,8 @@ type ImageConfig struct { // HostUser describes the host user type HostUser struct { - // Whether this machine should run in a rootfull or rootless manner - Rootfull bool + // Whether this machine should run in a rootful or rootless manner + Rootful bool // UID is the numerical id of the user that called machine UID int } diff --git a/pkg/machine/qemu/machine.go b/pkg/machine/qemu/machine.go index c54d18a4b..969acb760 100644 --- a/pkg/machine/qemu/machine.go +++ b/pkg/machine/qemu/machine.go @@ -76,7 +76,6 @@ func (p *Provider) NewMachine(opts machine.InitOptions) (machine.VM, error) { return nil, err } vm.IgnitionFilePath = *ignitionFile - imagePath, err := NewMachineFile(opts.ImagePath, nil) if err != nil { return nil, err @@ -206,7 +205,7 @@ func migrateVM(configPath string, config []byte, vm *MachineVM) error { vm.QMPMonitor = qmpMonitor vm.ReadySocket = readySocket vm.RemoteUsername = old.RemoteUsername - vm.Rootfull = old.Rootfull + vm.Rootful = old.Rootful vm.UID = old.UID // Backup the original config file @@ -260,7 +259,7 @@ func (v *MachineVM) Init(opts machine.InitOptions) (bool, error) { ) sshDir := filepath.Join(homedir.Get(), ".ssh") v.IdentityPath = filepath.Join(sshDir, v.Name) - v.Rootfull = opts.Rootfull + v.Rootful = opts.Rootful switch opts.ImagePath { case Testing, Next, Stable, "": @@ -358,8 +357,8 @@ func (v *MachineVM) Init(opts machine.InitOptions) (bool, error) { names := []string{v.Name, v.Name + "-root"} // The first connection defined when connections is empty will become the default - // regardless of IsDefault, so order according to rootfull - if opts.Rootfull { + // regardless of IsDefault, so order according to rootful + if opts.Rootful { uris[0], names[0], uris[1], names[1] = uris[1], names[1], uris[0], names[0] } @@ -375,7 +374,6 @@ func (v *MachineVM) Init(opts machine.InitOptions) (bool, error) { if err := v.writeConfig(); err != nil { return false, fmt.Errorf("writing JSON file: %w", err) } - // User has provided ignition file so keygen // will be skipped. if len(opts.IgnitionPath) < 1 { @@ -389,7 +387,6 @@ func (v *MachineVM) Init(opts machine.InitOptions) (bool, error) { if err := v.prepare(); err != nil { return false, err } - originalDiskSize, err := getDiskSize(v.getImageFile()) if err != nil { return false, err @@ -437,7 +434,7 @@ func (v *MachineVM) Init(opts machine.InitOptions) (bool, error) { } func (v *MachineVM) Set(_ string, opts machine.SetOptions) error { - if v.Rootfull == opts.Rootfull { + if v.Rootful == opts.Rootful { return nil } @@ -461,7 +458,7 @@ func (v *MachineVM) Set(_ string, opts machine.SetOptions) error { if changeCon { newDefault := v.Name - if opts.Rootfull { + if opts.Rootful { newDefault += "-root" } if err := machine.ChangeDefault(newDefault); err != nil { @@ -469,7 +466,7 @@ func (v *MachineVM) Set(_ string, opts machine.SetOptions) error { } } - v.Rootfull = opts.Rootfull + v.Rootful = opts.Rootful return v.writeConfig() } @@ -528,17 +525,28 @@ func (v *MachineVM) Start(name string, _ machine.StartOptions) error { time.Sleep(wait) wait++ } + defer qemuSocketConn.Close() if err != nil { return err } - fd, err := qemuSocketConn.(*net.UnixConn).File() if err != nil { return err } + defer fd.Close() + dnr, err := os.OpenFile("/dev/null", os.O_RDONLY, 0755) + if err != nil { + return err + } + defer dnr.Close() + dnw, err := os.OpenFile("/dev/null", os.O_WRONLY, 0755) + if err != nil { + return err + } + defer dnw.Close() attr := new(os.ProcAttr) - files := []*os.File{os.Stdin, os.Stdout, os.Stderr, fd} + files := []*os.File{dnr, dnw, dnw, fd} attr.Files = files logrus.Debug(v.CmdLine) cmd := v.CmdLine @@ -566,7 +574,7 @@ func (v *MachineVM) Start(name string, _ machine.StartOptions) error { } _, err = os.StartProcess(cmd[0], cmd, attr) if err != nil { - return err + return errors.Wrapf(err, "unable to execute %q", cmd) } } fmt.Println("Waiting for VM ...") @@ -589,11 +597,11 @@ func (v *MachineVM) Start(name string, _ machine.StartOptions) error { if err != nil { return err } + defer conn.Close() _, err = bufio.NewReader(conn).ReadString('\n') if err != nil { return err } - if len(v.Mounts) > 0 { state, err := v.State(true) if err != nil { @@ -944,7 +952,7 @@ func (v *MachineVM) SSH(_ string, opts machine.SSHOptions) error { sshDestination := username + "@localhost" port := strconv.Itoa(v.Port) - args := []string{"-i", v.IdentityPath, "-p", port, sshDestination, "-o", "UserKnownHostsFile /dev/null", "-o", "StrictHostKeyChecking no"} + args := []string{"-i", v.IdentityPath, "-p", port, sshDestination, "-o", "UserKnownHostsFile=/dev/null", "-o", "StrictHostKeyChecking=no"} if len(opts.Args) > 0 { args = append(args, opts.Args...) } else { @@ -1120,9 +1128,19 @@ func (v *MachineVM) startHostNetworking() (string, apiForwardingState, error) { } attr := new(os.ProcAttr) - // Pass on stdin, stdout, stderr - files := []*os.File{os.Stdin, os.Stdout, os.Stderr} - attr.Files = files + dnr, err := os.OpenFile("/dev/null", os.O_RDONLY, 0755) + if err != nil { + return "", noForwarding, err + } + dnw, err := os.OpenFile("/dev/null", os.O_WRONLY, 0755) + if err != nil { + return "", noForwarding, err + } + + defer dnr.Close() + defer dnw.Close() + + attr.Files = []*os.File{dnr, dnw, dnw} cmd := []string{binary} cmd = append(cmd, []string{"-listen-qemu", fmt.Sprintf("unix://%s", v.QMPMonitor.Address.GetPath()), "-pid-file", v.PidFilePath.GetPath()}...) // Add the ssh port @@ -1139,7 +1157,7 @@ func (v *MachineVM) startHostNetworking() (string, apiForwardingState, error) { fmt.Println(cmd) } _, err = os.StartProcess(cmd[0], cmd, attr) - return forwardSock, state, err + return forwardSock, state, errors.Wrapf(err, "unable to execute: %q", cmd) } func (v *MachineVM) setupAPIForwarding(cmd []string) ([]string, string, apiForwardingState) { @@ -1152,7 +1170,7 @@ func (v *MachineVM) setupAPIForwarding(cmd []string) ([]string, string, apiForwa destSock := fmt.Sprintf("/run/user/%d/podman/podman.sock", v.UID) forwardUser := "core" - if v.Rootfull { + if v.Rootful { destSock = "/run/podman/podman.sock" forwardUser = "root" } @@ -1358,11 +1376,11 @@ func (v *MachineVM) waitAPIAndPrintInfo(forwardState apiForwardingState, forward } waitAndPingAPI(forwardSock) - if !v.Rootfull { + if !v.Rootful { fmt.Printf("\nThis machine is currently configured in rootless mode. If your containers\n") fmt.Printf("require root permissions (e.g. ports < 1024), or if you run into compatibility\n") fmt.Printf("issues with non-podman clients, you can switch using the following command: \n") - fmt.Printf("\n\tpodman machine set --rootfull%s\n\n", suffix) + fmt.Printf("\n\tpodman machine set --rootful%s\n\n", suffix) } fmt.Printf("API forwarding listening on: %s\n", forwardSock) diff --git a/pkg/machine/wsl/machine.go b/pkg/machine/wsl/machine.go index 6e0453f8f..f57dbd299 100644 --- a/pkg/machine/wsl/machine.go +++ b/pkg/machine/wsl/machine.go @@ -165,8 +165,8 @@ type MachineVM struct { Port int // RemoteUsername of the vm user RemoteUsername string - // Whether this machine should run in a rootfull or rootless manner - Rootfull bool + // Whether this machine should run in a rootful or rootless manner + Rootful bool } type ExitCodeError struct { @@ -232,7 +232,7 @@ func (v *MachineVM) Init(opts machine.InitOptions) (bool, error) { homeDir := homedir.Get() sshDir := filepath.Join(homeDir, ".ssh") v.IdentityPath = filepath.Join(sshDir, v.Name) - v.Rootfull = opts.Rootfull + v.Rootful = opts.Rootful if err := downloadDistro(v, opts); err != nil { return false, err @@ -316,8 +316,8 @@ func setupConnections(v *MachineVM, opts machine.InitOptions, sshDir string) err names := []string{v.Name, v.Name + "-root"} // The first connection defined when connections is empty will become the default - // regardless of IsDefault, so order according to rootfull - if opts.Rootfull { + // regardless of IsDefault, so order according to rootful + if opts.Rootful { uris[0], names[0], uris[1], names[1] = uris[1], names[1], uris[0], names[0] } @@ -448,6 +448,10 @@ func configureSystem(v *MachineVM, dist string) error { return errors.Wrap(err, "could not create containers.conf for guest OS") } + if err := runCmdPassThrough("wsl", "-d", dist, "sh", "-c", "echo wsl > /etc/containers/podman-machine"); err != nil { + return errors.Wrap(err, "could not create podman-machine file for guest OS") + } + return nil } @@ -733,7 +737,7 @@ func pipeCmdPassThrough(name string, input string, arg ...string) error { } func (v *MachineVM) Set(name string, opts machine.SetOptions) error { - if v.Rootfull == opts.Rootfull { + if v.Rootful == opts.Rootful { return nil } @@ -744,7 +748,7 @@ func (v *MachineVM) Set(name string, opts machine.SetOptions) error { if changeCon { newDefault := v.Name - if opts.Rootfull { + if opts.Rootful { newDefault += "-root" } if err := machine.ChangeDefault(newDefault); err != nil { @@ -752,7 +756,7 @@ func (v *MachineVM) Set(name string, opts machine.SetOptions) error { } } - v.Rootfull = opts.Rootfull + v.Rootful = opts.Rootful return v.writeConfig() } @@ -768,7 +772,7 @@ func (v *MachineVM) Start(name string, _ machine.StartOptions) error { return errors.Wrap(err, "WSL bootstrap script failed") } - if !v.Rootfull { + if !v.Rootful { fmt.Printf("\nThis machine is currently configured in rootless mode. If your containers\n") fmt.Printf("require root permissions (e.g. ports < 1024), or if you run into compatibility\n") fmt.Printf("issues with non-podman clients, you can switch using the following command: \n") @@ -777,7 +781,7 @@ func (v *MachineVM) Start(name string, _ machine.StartOptions) error { if name != machine.DefaultMachineName { suffix = " " + name } - fmt.Printf("\n\tpodman machine set --rootfull%s\n\n", suffix) + fmt.Printf("\n\tpodman machine set --rootful%s\n\n", suffix) } globalName, pipeName, err := launchWinProxy(v) @@ -833,7 +837,7 @@ func launchWinProxy(v *MachineVM) (bool, string, error) { destSock := "/run/user/1000/podman/podman.sock" forwardUser := v.RemoteUsername - if v.Rootfull { + if v.Rootful { destSock = "/run/podman/podman.sock" forwardUser = "root" } diff --git a/pkg/signal/signal_common_test.go b/pkg/signal/signal_common_test.go new file mode 100644 index 000000000..c4ae6b389 --- /dev/null +++ b/pkg/signal/signal_common_test.go @@ -0,0 +1,120 @@ +package signal + +import ( + "syscall" + "testing" +) + +func TestParseSignal(t *testing.T) { + type args struct { + rawSignal string + } + tests := []struct { + name string + args args + want syscall.Signal + wantErr bool + }{ + { + name: "KILL to SIGKILL", + args: args{ + rawSignal: "KILL", + }, + want: syscall.SIGKILL, + wantErr: false, + }, + { + name: "Case doesnt matter", + args: args{ + rawSignal: "kIlL", + }, + want: syscall.SIGKILL, + wantErr: false, + }, + { + name: "Garbage signal", + args: args{ + rawSignal: "FOO", + }, + want: -1, + wantErr: true, + }, + { + name: "Signal with prepended SIG", + args: args{ + rawSignal: "SIGKILL", + }, + want: syscall.SIGKILL, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := ParseSignal(tt.args.rawSignal) + if (err != nil) != tt.wantErr { + t.Errorf("ParseSignal() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got != tt.want { + t.Errorf("ParseSignal() got = %v, want %v", got, tt.want) + } + }) + } +} + +func TestParseSignalNameOrNumber(t *testing.T) { + type args struct { + rawSignal string + } + tests := []struct { + name string + args args + want syscall.Signal + wantErr bool + }{ + { + name: "Kill should work", + args: args{ + rawSignal: "kill", + }, + want: syscall.SIGKILL, + wantErr: false, + }, + { + name: "9 for kill should work", + args: args{ + rawSignal: "9", + }, + want: syscall.SIGKILL, + wantErr: false, + }, + { + name: "Non-defined signal number should work", + args: args{ + rawSignal: "923", + }, + want: 923, + wantErr: false, + }, + { + name: "garbage should fail", + args: args{ + rawSignal: "foo", + }, + want: -1, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := ParseSignalNameOrNumber(tt.args.rawSignal) + if (err != nil) != tt.wantErr { + t.Errorf("ParseSignalNameOrNumber() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got != tt.want { + t.Errorf("ParseSignalNameOrNumber() got = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/pkg/specgen/generate/container_create.go b/pkg/specgen/generate/container_create.go index 021b88280..50454cbab 100644 --- a/pkg/specgen/generate/container_create.go +++ b/pkg/specgen/generate/container_create.go @@ -146,13 +146,13 @@ func MakeContainer(ctx context.Context, rt *libpod.Runtime, s *specgen.SpecGener options = append(options, libpod.WithHostUsers(s.HostUsers)) } - command, err := makeCommand(ctx, s, imageData, rtc) + command, err := makeCommand(s, imageData, rtc) if err != nil { return nil, nil, nil, err } infraVol := (len(compatibleOptions.Mounts) > 0 || len(compatibleOptions.Volumes) > 0 || len(compatibleOptions.ImageVolumes) > 0 || len(compatibleOptions.OverlayVolumes) > 0) - opts, err := createContainerOptions(ctx, rt, s, pod, finalVolumes, finalOverlays, imageData, command, infraVol, *compatibleOptions) + opts, err := createContainerOptions(rt, s, pod, finalVolumes, finalOverlays, imageData, command, infraVol, *compatibleOptions) if err != nil { return nil, nil, nil, err } @@ -251,7 +251,7 @@ func isCDIDevice(device string) bool { return cdi.IsQualifiedName(device) } -func createContainerOptions(ctx context.Context, rt *libpod.Runtime, s *specgen.SpecGenerator, pod *libpod.Pod, volumes []*specgen.NamedVolume, overlays []*specgen.OverlayVolume, imageData *libimage.ImageData, command []string, infraVolumes bool, compatibleOptions libpod.InfraInherit) ([]libpod.CtrCreateOption, error) { +func createContainerOptions(rt *libpod.Runtime, s *specgen.SpecGenerator, pod *libpod.Pod, volumes []*specgen.NamedVolume, overlays []*specgen.OverlayVolume, imageData *libimage.ImageData, command []string, infraVolumes bool, compatibleOptions libpod.InfraInherit) ([]libpod.CtrCreateOption, error) { var options []libpod.CtrCreateOption var err error @@ -453,7 +453,7 @@ func createContainerOptions(ctx context.Context, rt *libpod.Runtime, s *specgen. options = append(options, libpod.WithPrivileged(s.Privileged)) // Get namespace related options - namespaceOpts, err := namespaceOptions(ctx, s, rt, pod, imageData) + namespaceOpts, err := namespaceOptions(s, rt, pod, imageData) if err != nil { return nil, err } diff --git a/pkg/specgen/generate/namespaces.go b/pkg/specgen/generate/namespaces.go index d8d1ae652..2362f61c4 100644 --- a/pkg/specgen/generate/namespaces.go +++ b/pkg/specgen/generate/namespaces.go @@ -1,7 +1,6 @@ package generate import ( - "context" "fmt" "os" "strings" @@ -80,7 +79,7 @@ func GetDefaultNamespaceMode(nsType string, cfg *config.Config, pod *libpod.Pod) // joining a pod. // TODO: Consider grouping options that are not directly attached to a namespace // elsewhere. -func namespaceOptions(ctx context.Context, s *specgen.SpecGenerator, rt *libpod.Runtime, pod *libpod.Pod, imageData *libimage.ImageData) ([]libpod.CtrCreateOption, error) { +func namespaceOptions(s *specgen.SpecGenerator, rt *libpod.Runtime, pod *libpod.Pod, imageData *libimage.ImageData) ([]libpod.CtrCreateOption, error) { toReturn := []libpod.CtrCreateOption{} // If pod is not nil, get infra container. @@ -256,7 +255,7 @@ func namespaceOptions(ctx context.Context, s *specgen.SpecGenerator, rt *libpod. } toReturn = append(toReturn, libpod.WithNetNSFrom(netCtr)) case specgen.Slirp: - portMappings, expose, err := createPortMappings(ctx, s, imageData) + portMappings, expose, err := createPortMappings(s, imageData) if err != nil { return nil, err } @@ -268,7 +267,7 @@ func namespaceOptions(ctx context.Context, s *specgen.SpecGenerator, rt *libpod. case specgen.Private: fallthrough case specgen.Bridge: - portMappings, expose, err := createPortMappings(ctx, s, imageData) + portMappings, expose, err := createPortMappings(s, imageData) if err != nil { return nil, err } diff --git a/pkg/specgen/generate/oci.go b/pkg/specgen/generate/oci.go index 961cea933..95bcea8f0 100644 --- a/pkg/specgen/generate/oci.go +++ b/pkg/specgen/generate/oci.go @@ -32,7 +32,7 @@ func setProcOpts(s *specgen.SpecGenerator, g *generate.Generator) { } } -func addRlimits(s *specgen.SpecGenerator, g *generate.Generator) error { +func addRlimits(s *specgen.SpecGenerator, g *generate.Generator) { var ( isRootless = rootless.IsRootless() nofileSet = false @@ -41,7 +41,7 @@ func addRlimits(s *specgen.SpecGenerator, g *generate.Generator) error { if s.Rlimits == nil { g.Config.Process.Rlimits = nil - return nil + return } for _, u := range s.Rlimits { @@ -91,12 +91,10 @@ func addRlimits(s *specgen.SpecGenerator, g *generate.Generator) error { } g.AddProcessRlimits("RLIMIT_NPROC", max, current) } - - return nil } // Produce the final command for the container. -func makeCommand(ctx context.Context, s *specgen.SpecGenerator, imageData *libimage.ImageData, rtc *config.Config) ([]string, error) { +func makeCommand(s *specgen.SpecGenerator, imageData *libimage.ImageData, rtc *config.Config) ([]string, error) { finalCommand := []string{} entrypoint := s.Entrypoint @@ -388,9 +386,7 @@ func SpecGenToOCI(ctx context.Context, s *specgen.SpecGenerator, rt *libpod.Runt g.AddProcessEnv(name, val) } - if err := addRlimits(s, &g); err != nil { - return nil, err - } + addRlimits(s, &g) // NAMESPACES if err := specConfigureNamespaces(s, &g, rt, pod); err != nil { diff --git a/pkg/specgen/generate/pod_create.go b/pkg/specgen/generate/pod_create.go index ba823f3a8..a3408b402 100644 --- a/pkg/specgen/generate/pod_create.go +++ b/pkg/specgen/generate/pod_create.go @@ -119,7 +119,7 @@ func MakePod(p *entities.PodSpec, rt *libpod.Runtime) (*libpod.Pod, error) { } } - options, err := createPodOptions(&p.PodSpecGen, rt, p.PodSpecGen.InfraContainerSpec) + options, err := createPodOptions(&p.PodSpecGen) if err != nil { return nil, err } @@ -161,11 +161,11 @@ func MakePod(p *entities.PodSpec, rt *libpod.Runtime) (*libpod.Pod, error) { return pod, nil } -func createPodOptions(p *specgen.PodSpecGenerator, rt *libpod.Runtime, infraSpec *specgen.SpecGenerator) ([]libpod.PodCreateOption, error) { +func createPodOptions(p *specgen.PodSpecGenerator) ([]libpod.PodCreateOption, error) { var ( options []libpod.PodCreateOption ) - if !p.NoInfra { //&& infraSpec != nil { + if !p.NoInfra { options = append(options, libpod.WithInfraContainer()) if p.ShareParent == nil || (p.ShareParent != nil && *p.ShareParent) { options = append(options, libpod.WithPodParent()) diff --git a/pkg/specgen/generate/ports.go b/pkg/specgen/generate/ports.go index c30c4e49d..bec548d3b 100644 --- a/pkg/specgen/generate/ports.go +++ b/pkg/specgen/generate/ports.go @@ -1,7 +1,6 @@ package generate import ( - "context" "fmt" "net" "sort" @@ -338,7 +337,7 @@ func appendProtocolsNoDuplicates(slice []string, protocols []string) []string { } // Make final port mappings for the container -func createPortMappings(ctx context.Context, s *specgen.SpecGenerator, imageData *libimage.ImageData) ([]types.PortMapping, map[uint16][]string, error) { +func createPortMappings(s *specgen.SpecGenerator, imageData *libimage.ImageData) ([]types.PortMapping, map[uint16][]string, error) { expose := make(map[uint16]string) var err error if imageData != nil { diff --git a/pkg/specgen/volumes.go b/pkg/specgen/volumes.go index eca8c0c35..b26666df3 100644 --- a/pkg/specgen/volumes.go +++ b/pkg/specgen/volumes.go @@ -65,7 +65,7 @@ func GenVolumeMounts(volumeFlag []string) (map[string]spec.Mount, map[string]*Na err error ) - splitVol := strings.Split(vol, ":") + splitVol := SplitVolumeString(vol) if len(splitVol) > 3 { return nil, nil, nil, errors.Wrapf(volumeFormatErr, vol) } @@ -93,7 +93,7 @@ func GenVolumeMounts(volumeFlag []string) (map[string]spec.Mount, map[string]*Na } } - if strings.HasPrefix(src, "/") || strings.HasPrefix(src, ".") { + if strings.HasPrefix(src, "/") || strings.HasPrefix(src, ".") || isHostWinPath(src) { // This is not a named volume overlayFlag := false chownFlag := false @@ -152,3 +152,26 @@ func GenVolumeMounts(volumeFlag []string) (map[string]spec.Mount, map[string]*Na return mounts, volumes, overlayVolumes, nil } + +// Splits a volume string, accounting for Win drive paths +// when running as a WSL linux guest or Windows client +func SplitVolumeString(vol string) []string { + parts := strings.Split(vol, ":") + if !shouldResolveWinPaths() { + return parts + } + + // Skip extended marker prefix if present + n := 0 + if strings.HasPrefix(vol, `\\?\`) { + n = 4 + } + + if hasWinDriveScheme(vol, n) { + first := parts[0] + ":" + parts[1] + parts = parts[1:] + parts[0] = first + } + + return parts +} diff --git a/pkg/specgen/winpath.go b/pkg/specgen/winpath.go new file mode 100644 index 000000000..f4249fab1 --- /dev/null +++ b/pkg/specgen/winpath.go @@ -0,0 +1,59 @@ +package specgen + +import ( + "fmt" + "strings" + "unicode" + + "github.com/pkg/errors" +) + +func isHostWinPath(path string) bool { + return shouldResolveWinPaths() && strings.HasPrefix(path, `\\`) || hasWinDriveScheme(path, 0) || winPathExists(path) +} + +func hasWinDriveScheme(path string, start int) bool { + if len(path) < start+2 || path[start+1] != ':' { + return false + } + + drive := rune(path[start]) + return drive < unicode.MaxASCII && unicode.IsLetter(drive) +} + +// Converts a Windows path to a WSL guest path if local env is a WSL linux guest or this is a Windows client. +func ConvertWinMountPath(path string) (string, error) { + if !shouldResolveWinPaths() { + return path, nil + } + + if strings.HasPrefix(path, "/") { + // Handle /[driveletter]/windows/path form (e.g. c:\Users\bar == /c/Users/bar) + if len(path) > 2 && path[2] == '/' && shouldResolveUnixWinVariant(path) { + drive := unicode.ToLower(rune(path[1])) + if unicode.IsLetter(drive) && drive <= unicode.MaxASCII { + return fmt.Sprintf("/mnt/%c/%s", drive, path[3:]), nil + } + } + + // unix path - pass through + return path, nil + } + + // Convert remote win client relative paths to absolute + path = resolveRelativeOnWindows(path) + + // Strip extended marker prefix if present + path = strings.TrimPrefix(path, `\\?\`) + + // Drive installed via wsl --mount + if strings.HasPrefix(path, `\\.\`) { + path = "/mnt/wsl/" + path[4:] + } else if len(path) > 1 && path[1] == ':' { + path = "/mnt/" + strings.ToLower(path[0:1]) + path[2:] + } else { + return path, errors.New("unsupported UNC path") + } + + return strings.ReplaceAll(path, `\`, "/"), nil +} diff --git a/pkg/specgen/winpath_linux.go b/pkg/specgen/winpath_linux.go new file mode 100644 index 000000000..f42ac7639 --- /dev/null +++ b/pkg/specgen/winpath_linux.go @@ -0,0 +1,24 @@ +package specgen + +import ( + "os" + + "github.com/containers/common/pkg/machine" +) + +func shouldResolveWinPaths() bool { + return machine.MachineHostType() == "wsl" +} + +func shouldResolveUnixWinVariant(path string) bool { + _, err := os.Stat(path) + return err != nil +} + +func resolveRelativeOnWindows(path string) string { + return path +} + +func winPathExists(path string) bool { + return false +} diff --git a/pkg/specgen/winpath_unsupported.go b/pkg/specgen/winpath_unsupported.go new file mode 100644 index 000000000..4cd008fdd --- /dev/null +++ b/pkg/specgen/winpath_unsupported.go @@ -0,0 +1,20 @@ +//go:build !linux && !windows +// +build !linux,!windows + +package specgen + +func shouldResolveWinPaths() bool { + return false +} + +func shouldResolveUnixWinVariant(path string) bool { + return false +} + +func resolveRelativeOnWindows(path string) string { + return path +} + +func winPathExists(path string) bool { + return false +} diff --git a/pkg/specgen/winpath_windows.go b/pkg/specgen/winpath_windows.go new file mode 100644 index 000000000..c6aad314a --- /dev/null +++ b/pkg/specgen/winpath_windows.go @@ -0,0 +1,30 @@ +package specgen + +import ( + "github.com/sirupsen/logrus" + "os" + "path/filepath" +) + +func shouldResolveUnixWinVariant(path string) bool { + return true +} + +func shouldResolveWinPaths() bool { + return true +} + +func resolveRelativeOnWindows(path string) string { + ret, err := filepath.Abs(path) + if err != nil { + logrus.Debugf("problem resolving possible relative path %q: %s", path, err.Error()) + return path + } + + return ret +} + +func winPathExists(path string) bool { + _, err := os.Stat(path) + return err == nil +} diff --git a/pkg/specgenutil/ports_test.go b/pkg/specgenutil/ports_test.go new file mode 100644 index 000000000..3f62c619c --- /dev/null +++ b/pkg/specgenutil/ports_test.go @@ -0,0 +1,57 @@ +package specgenutil + +import "testing" + +func Test_verifyExpose(t *testing.T) { + type args struct { + expose []string + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "single port with tcp", + args: args{ + expose: []string{"53/tcp"}, + }, + wantErr: false, + }, + { + name: "single port with udp", + args: args{ + expose: []string{"53/udp"}, + }, + wantErr: false, + }, + { + name: "good port range", + args: args{ + expose: []string{"100-133"}, + }, + wantErr: false, + }, + { + name: "high to low should fail", + args: args{ + expose: []string{"100-99"}, + }, + wantErr: true, + }, + { + name: "range with protocol", + args: args{ + expose: []string{"53/tcp-55/tcp"}, + }, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := verifyExpose(tt.args.expose); (err != nil) != tt.wantErr { + t.Errorf("verifyExpose() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/pkg/specgenutil/specgen.go b/pkg/specgenutil/specgen.go index 00de99817..f0dfcac1a 100644 --- a/pkg/specgenutil/specgen.go +++ b/pkg/specgenutil/specgen.go @@ -136,7 +136,7 @@ func LimitToSwap(memory *specs.LinuxMemory, swap string, ml int64) { } } -func getMemoryLimits(s *specgen.SpecGenerator, c *entities.ContainerCreateOptions) (*specs.LinuxMemory, error) { +func getMemoryLimits(c *entities.ContainerCreateOptions) (*specs.LinuxMemory, error) { var err error memory := &specs.LinuxMemory{} hasLimits := false @@ -497,7 +497,7 @@ func FillOutSpecGen(s *specgen.SpecGenerator, c *entities.ContainerCreateOptions } if s.ResourceLimits.Memory == nil || (len(c.Memory) != 0 || len(c.MemoryReservation) != 0 || len(c.MemorySwap) != 0 || c.MemorySwappiness != 0) { - s.ResourceLimits.Memory, err = getMemoryLimits(s, c) + s.ResourceLimits.Memory, err = getMemoryLimits(c) if err != nil { return err } diff --git a/pkg/specgenutil/specgenutil_test.go b/pkg/specgenutil/specgenutil_test.go new file mode 100644 index 000000000..5867b0ae0 --- /dev/null +++ b/pkg/specgenutil/specgenutil_test.go @@ -0,0 +1,77 @@ +//go:build linux +// +build linux + +package specgenutil + +import ( + "testing" + + "github.com/containers/common/pkg/machine" + "github.com/containers/podman/v4/pkg/domain/entities" + "github.com/containers/podman/v4/pkg/specgen" + "github.com/stretchr/testify/assert" +) + +func TestWinPath(t *testing.T) { + const ( + fail = false + pass = true + ) + tests := []struct { + vol string + source string + dest string + isN bool + outcome bool + mach string + }{ + {`C:\Foo:/blah`, "/mnt/c/Foo", "/blah", false, pass, "wsl"}, + {`C:\Foo:/blah`, "/mnt/c/Foo", "/blah", false, fail, ""}, + {`\\?\C:\Foo:/blah`, "/mnt/c/Foo", "/blah", false, pass, "wsl"}, + {`/c/bar:/blah`, "/mnt/c/bar", "/blah", false, pass, "wsl"}, + {`/c/bar:/blah`, "/c/bar", "/blah", false, pass, ""}, + {`/test/this:/blah`, "/test/this", "/blah", false, pass, "wsl"}, + {`c:/bar/something:/other`, "/mnt/c/bar/something", "/other", false, pass, "wsl"}, + {`c:/foo:ro`, "c", "/foo", true, pass, ""}, + {`\\computer\loc:/dest`, "", "", false, fail, "wsl"}, + {`\\.\drive\loc:/target`, "/mnt/wsl/drive/loc", "/target", false, pass, "wsl"}, + } + + f := func(vol string, mach string) (*specgen.SpecGenerator, error) { + machine := machine.GetMachineMarker() + oldEnable, oldType := machine.Enabled, machine.Type + machine.Enabled, machine.Type = len(mach) > 0, mach + sg := specgen.NewSpecGenerator("nothing", false) + err := FillOutSpecGen(sg, &entities.ContainerCreateOptions{ + ImageVolume: "ignore", + Volume: []string{vol}}, []string{}, + ) + machine.Enabled, machine.Type = oldEnable, oldType + return sg, err + } + + for _, test := range tests { + msg := "Checking: " + test.vol + sg, err := f(test.vol, test.mach) + if test.outcome == fail { + assert.NotNil(t, err, msg) + continue + } + if !assert.Nil(t, err, msg) { + continue + } + if test.isN { + if !assert.Equal(t, 1, len(sg.Volumes), msg) { + continue + } + assert.Equal(t, test.source, sg.Volumes[0].Name, msg) + assert.Equal(t, test.dest, sg.Volumes[0].Dest, msg) + } else { + if !assert.Equal(t, 1, len(sg.Mounts), msg) { + continue + } + assert.Equal(t, test.source, sg.Mounts[0].Source, msg) + assert.Equal(t, test.dest, sg.Mounts[0].Destination, msg) + } + } +} diff --git a/pkg/specgenutil/util.go b/pkg/specgenutil/util.go index 80d31398b..fa2e90457 100644 --- a/pkg/specgenutil/util.go +++ b/pkg/specgenutil/util.go @@ -281,6 +281,7 @@ func CreateExitCommandArgs(storageConfig storageTypes.StoreOptions, config *conf "--tmpdir", config.Engine.TmpDir, "--network-config-dir", config.Network.NetworkConfigDir, "--network-backend", config.Network.NetworkBackend, + "--volumepath", config.Engine.VolumePath, } if config.Engine.OCIRuntime != "" { command = append(command, []string{"--runtime", config.Engine.OCIRuntime}...) diff --git a/pkg/specgenutil/util_test.go b/pkg/specgenutil/util_test.go new file mode 100644 index 000000000..79d60d335 --- /dev/null +++ b/pkg/specgenutil/util_test.go @@ -0,0 +1,146 @@ +package specgenutil + +import ( + "reflect" + "testing" +) + +func TestCreateExpose(t *testing.T) { + single := make(map[uint16]string, 0) + single[99] = "tcp" + + simpleRange := make(map[uint16]string, 0) + simpleRange[99] = "tcp" + simpleRange[100] = "tcp" + + simpleRangeUDP := make(map[uint16]string, 0) + simpleRangeUDP[99] = "udp" + simpleRangeUDP[100] = "udp" + type args struct { + expose []string + } + tests := []struct { + name string + args args + want map[uint16]string + wantErr bool + }{ + { + name: "single port", + args: args{ + expose: []string{"99"}, + }, + want: single, + wantErr: false, + }, + { + name: "simple range tcp", + args: args{ + expose: []string{"99-100"}, + }, + want: simpleRange, + wantErr: false, + }, + { + name: "simple range udp", + args: args{ + expose: []string{"99-100/udp"}, + }, + want: simpleRangeUDP, + wantErr: false, + }, + { + name: "range inverted should fail", + args: args{ + expose: []string{"100-99"}, + }, + want: nil, + wantErr: true, + }, + { + name: "specifying protocol twice should fail", + args: args{ + expose: []string{"99/tcp-100/tcp"}, + }, + want: nil, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := CreateExpose(tt.args.expose) + if (err != nil) != tt.wantErr { + t.Errorf("CreateExpose() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("CreateExpose() got = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_parseAndValidatePort(t *testing.T) { + type args struct { + port string + } + tests := []struct { + name string + args args + want uint16 + wantErr bool + }{ + { + name: "0 should fail", + args: args{ + port: "0", + }, + want: 0, + wantErr: true, + }, + { + name: "over 65535 should fail", + args: args{ + port: "66666", + }, + want: 0, + wantErr: true, + }, + { + name: "", + args: args{ + port: "99", + }, + want: 99, + wantErr: false, + }, + { + name: "negative values should fail", + args: args{ + port: "-1", + }, + want: 0, + wantErr: true, + }, + { + name: "protocol should fail", + args: args{ + port: "99/tcp", + }, + want: 0, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := parseAndValidatePort(tt.args.port) + if (err != nil) != tt.wantErr { + t.Errorf("parseAndValidatePort() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got != tt.want { + t.Errorf("parseAndValidatePort() got = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/pkg/specgenutil/volumes.go b/pkg/specgenutil/volumes.go index 95ce420f8..50d745380 100644 --- a/pkg/specgenutil/volumes.go +++ b/pkg/specgenutil/volumes.go @@ -3,7 +3,7 @@ package specgenutil import ( "encoding/csv" "fmt" - "path/filepath" + "path" "strings" "github.com/containers/common/pkg/parse" @@ -123,7 +123,7 @@ func parseVolumes(volumeFlag, mountFlag, tmpfsFlag []string, addReadOnlyTmpfs bo finalMounts := make([]spec.Mount, 0, len(unifiedMounts)) for _, mount := range unifiedMounts { if mount.Type == define.TypeBind { - absSrc, err := filepath.Abs(mount.Source) + absSrc, err := specgen.ConvertWinMountPath(mount.Source) if err != nil { return nil, nil, nil, nil, errors.Wrapf(err, "error getting absolute path of %s", mount.Source) } @@ -334,7 +334,7 @@ func getBindMount(args []string) (spec.Mount, error) { if err := parse.ValidateVolumeCtrDir(kv[1]); err != nil { return newMount, err } - newMount.Destination = filepath.Clean(kv[1]) + newMount.Destination = unixPathClean(kv[1]) setDest = true case "relabel": if setRelabel { @@ -456,7 +456,7 @@ func getTmpfsMount(args []string) (spec.Mount, error) { if err := parse.ValidateVolumeCtrDir(kv[1]); err != nil { return newMount, err } - newMount.Destination = filepath.Clean(kv[1]) + newMount.Destination = unixPathClean(kv[1]) setDest = true case "U", "chown": if setOwnership { @@ -507,7 +507,7 @@ func getDevptsMount(args []string) (spec.Mount, error) { if err := parse.ValidateVolumeCtrDir(kv[1]); err != nil { return newMount, err } - newMount.Destination = filepath.Clean(kv[1]) + newMount.Destination = unixPathClean(kv[1]) setDest = true default: return newMount, errors.Wrapf(util.ErrBadMntOption, "%s", kv[0]) @@ -572,7 +572,7 @@ func getNamedVolume(args []string) (*specgen.NamedVolume, error) { if err := parse.ValidateVolumeCtrDir(kv[1]); err != nil { return nil, err } - newVolume.Dest = filepath.Clean(kv[1]) + newVolume.Dest = unixPathClean(kv[1]) setDest = true case "U", "chown": if setOwnership { @@ -624,7 +624,7 @@ func getImageVolume(args []string) (*specgen.ImageVolume, error) { if err := parse.ValidateVolumeCtrDir(kv[1]); err != nil { return nil, err } - newVolume.Destination = filepath.Clean(kv[1]) + newVolume.Destination = unixPathClean(kv[1]) case "rw", "readwrite": switch kv[1] { case "true": @@ -670,7 +670,7 @@ func getTmpfsMounts(tmpfsFlag []string) (map[string]spec.Mount, error) { } mount := spec.Mount{ - Destination: filepath.Clean(destPath), + Destination: unixPathClean(destPath), Type: define.TypeTmpfs, Options: options, Source: define.TypeTmpfs, @@ -700,3 +700,8 @@ func validChownFlag(flag string) (bool, error) { return true, nil } + +// Use path instead of filepath to preserve Unix style paths on Windows +func unixPathClean(p string) string { + return path.Clean(p) +} diff --git a/pkg/specgenutil/volumes_test.go b/pkg/specgenutil/volumes_test.go new file mode 100644 index 000000000..fc6caf83c --- /dev/null +++ b/pkg/specgenutil/volumes_test.go @@ -0,0 +1,68 @@ +package specgenutil + +import "testing" + +func Test_validChownFlag(t *testing.T) { + type args struct { + flag string + } + tests := []struct { + name string + args args + want bool + wantErr bool + }{ + { + name: "U true", + args: args{ + flag: "U=true", + }, + want: true, + wantErr: false, + }, + { + name: "U true case doesnt matter", + args: args{ + flag: "u=True", + }, + want: true, + wantErr: false, + }, + { + name: "U is false", + args: args{ + flag: "U=false", + }, + want: false, + wantErr: false, + }, + { + name: "chown should also work", + args: args{ + flag: "chown=true", + }, + want: true, + wantErr: false, + }, + { + name: "garbage value should fail", + args: args{ + flag: "U=foobar", + }, + want: false, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := validChownFlag(tt.args.flag) + if (err != nil) != tt.wantErr { + t.Errorf("validChownFlag() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got != tt.want { + t.Errorf("validChownFlag() got = %v, want %v", got, tt.want) + } + }) + } +} |