diff options
25 files changed, 1149 insertions, 28 deletions
@@ -537,7 +537,7 @@ localunit: test/goecho/goecho test/version/version UNIT=1 $(GOBIN)/ginkgo \ -r \ $(TESTFLAGS) \ - --skipPackage test/e2e,pkg/apparmor,pkg/bindings,hack \ + --skipPackage test/e2e,pkg/apparmor,pkg/bindings,hack,pkg/machine/e2e \ --cover \ --covermode atomic \ --coverprofile coverprofile \ diff --git a/cmd/podman/machine/init.go b/cmd/podman/machine/init.go index 2d0afbf05..4991c6aa3 100644 --- a/cmd/podman/machine/init.go +++ b/cmd/podman/machine/init.go @@ -135,7 +135,6 @@ func initMachine(cmd *cobra.Command, args []string) error { if err != nil { return err } - if finished, err := vm.Init(initOpts); err != nil || !finished { // Finished = true, err = nil - Success! Log a message with further instructions // Finished = false, err = nil - The installation is partially complete and podman should @@ -144,7 +143,6 @@ func initMachine(cmd *cobra.Command, args []string) error { // - a user has chosen to perform their own reboot // - reexec for limited admin operations, returning to parent // Finished = *, err != nil - Exit with an error message - return err } fmt.Println("Machine init complete") diff --git a/hack/golangci-lint.sh b/hack/golangci-lint.sh index bcb83a2fd..8b80bd9c9 100755 --- a/hack/golangci-lint.sh +++ b/hack/golangci-lint.sh @@ -10,9 +10,9 @@ BUILD_TAGS[abi]="${BUILD_TAGS[default]},!remoteclient" BUILD_TAGS[tunnel]="${BUILD_TAGS[default]},remote,remoteclient" declare -A SKIP_DIRS -SKIP_DIRS[abi]="" +SKIP_DIRS[abi]="pkg/machine/e2e" # TODO: add "remote" build tag to pkg/api -SKIP_DIRS[tunnel]="pkg/api" +SKIP_DIRS[tunnel]="pkg/api,pkg/machine/e2e" [[ $1 == run ]] && shift 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/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/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 872ca889e..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,unparam +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/qemu/machine.go b/pkg/machine/qemu/machine.go index c57fa32fb..69a986102 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 @@ -373,7 +372,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 { @@ -387,7 +385,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 @@ -514,17 +511,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 @@ -552,7 +560,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 ...") @@ -575,11 +583,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() if err != nil { @@ -918,7 +926,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 { @@ -1085,9 +1093,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 @@ -1104,7 +1122,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) { |