diff --git a/.cirrus.yml b/.cirrus.yml index a7bc08099b3..175cf7ff0dd 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -801,7 +801,7 @@ podman_machine_aarch64_task: depends_on: *build ec2_instance: <<: *standard_build_ec2_aarch64 - timeout_in: 30m + timeout_in: 40m env: TEST_FLAVOR: "machine-linux" TEST_BUILD_TAGS: "" diff --git a/cmd/podman/machine/init.go b/cmd/podman/machine/init.go index acae358c1af..616e3a025b9 100644 --- a/cmd/podman/machine/init.go +++ b/cmd/podman/machine/init.go @@ -166,6 +166,9 @@ func init() { providerFlagName := "provider" flags.StringVar(&providerOverride, providerFlagName, "", "Override the default machine provider") _ = initCmd.RegisterFlagCompletionFunc(providerFlagName, autocompleteMachineProvider) + + setDefaultConnectionFlagName := "update-connection" + flags.BoolVarP(&setDefaultSystemConn, setDefaultConnectionFlagName, "u", false, "Set default system connection for this machine") } func initMachine(cmd *cobra.Command, args []string) error { @@ -296,6 +299,7 @@ func initMachine(cmd *cobra.Command, args []string) error { if now { return start(cmd, args) } + extra := "" if initOpts.Name != defaultMachineName { extra = " " + initOpts.Name diff --git a/cmd/podman/machine/start.go b/cmd/podman/machine/start.go index 824acbff69b..9a43509bb37 100644 --- a/cmd/podman/machine/start.go +++ b/cmd/podman/machine/start.go @@ -23,7 +23,8 @@ var ( Example: `podman machine start podman-machine-default`, ValidArgsFunction: autocompleteMachine, } - startOpts = machine.StartOptions{} + startOpts = machine.StartOptions{} + setDefaultSystemConn bool ) func init() { @@ -38,15 +39,13 @@ func init() { quietFlagName := "quiet" flags.BoolVarP(&startOpts.Quiet, quietFlagName, "q", false, "Suppress machine starting status output") -} -func start(_ *cobra.Command, args []string) error { - var ( - err error - ) + setDefaultConnectionFlagName := "update-connection" + flags.BoolVarP(&setDefaultSystemConn, setDefaultConnectionFlagName, "u", false, "Set default system connection for this machine") +} +func start(cmd *cobra.Command, args []string) error { startOpts.NoInfo = startOpts.Quiet || startOpts.NoInfo - vmName := defaultMachineName if len(args) > 0 && len(args[0]) > 0 { vmName = args[0] @@ -61,10 +60,18 @@ func start(_ *cobra.Command, args []string) error { fmt.Printf("Starting machine %q\n", vmName) } - if err := shim.Start(mc, vmProvider, startOpts); err != nil { + shouldUpdate := processSystemConnUpdate(cmd, setDefaultSystemConn) + if err := shim.Start(mc, vmProvider, startOpts, shouldUpdate); err != nil { return err } fmt.Printf("Machine %q started successfully\n", vmName) newMachineEvent(events.Start, events.Event{Name: vmName}) return nil } + +func processSystemConnUpdate(cmd *cobra.Command, updateVal bool) *bool { + if !cmd.Flags().Changed("update-connection") { + return nil + } + return &updateVal +} diff --git a/docs/source/markdown/.gitignore b/docs/source/markdown/.gitignore index 3dd3b2e7d44..f2140cb301f 100644 --- a/docs/source/markdown/.gitignore +++ b/docs/source/markdown/.gitignore @@ -28,6 +28,7 @@ podman-logs.1.md podman-machine-init.1.md podman-machine-list.1.md podman-machine-set.1.md +podman-machine-start.1.md podman-manifest-add.1.md podman-manifest-annotate.1.md podman-manifest-create.1.md diff --git a/docs/source/markdown/options/update-connection.md b/docs/source/markdown/options/update-connection.md new file mode 100644 index 00000000000..c175178cca2 --- /dev/null +++ b/docs/source/markdown/options/update-connection.md @@ -0,0 +1,14 @@ +####> This option file is used in: +####> podman machine init, machine start +####> If file is edited, make sure the changes +####> are applicable to all of those. +#### **--update-connection**, **-u** + +When used in conjunction with `podman machine init --now` or `podman machine start`, this option sets the +associated machine system connection as the default. When using this option, a `-u` or `-update-connection` will +set the value to true. To set this value to false, meaning no change and no prompting, +use `--update-connection=false`. + +If the value is set to true, the machine connection will be set as the system default. +If the value is set to false, the system default will be unchanged. +If the option is not set, the user will be prompted and asked if it should be changed. diff --git a/docs/source/markdown/podman-machine-init.1.md.in b/docs/source/markdown/podman-machine-init.1.md.in index fe15107af15..348c91c24a5 100644 --- a/docs/source/markdown/podman-machine-init.1.md.in +++ b/docs/source/markdown/podman-machine-init.1.md.in @@ -127,6 +127,8 @@ as the host Windows operating system. @@option tls-verify +@@option update-connection + #### **--usb**=*bus=number,devnum=number* or *vendor=hexadecimal,product=hexadecimal* Assign a USB device from the host to the VM via USB passthrough. diff --git a/docs/source/markdown/podman-machine-start.1.md b/docs/source/markdown/podman-machine-start.1.md.in similarity index 98% rename from docs/source/markdown/podman-machine-start.1.md rename to docs/source/markdown/podman-machine-start.1.md.in index 4ae32485cb9..bc15fe8c915 100644 --- a/docs/source/markdown/podman-machine-start.1.md +++ b/docs/source/markdown/podman-machine-start.1.md.in @@ -39,6 +39,8 @@ Suppress informational tips. Suppress machine starting status output. +@@option update-connection + ## EXAMPLES Start the specified podman machine. diff --git a/pkg/machine/e2e/config_init_test.go b/pkg/machine/e2e/config_init_test.go index 33aafb6dcde..ae68d0c5fb0 100644 --- a/pkg/machine/e2e/config_init_test.go +++ b/pkg/machine/e2e/config_init_test.go @@ -1,6 +1,7 @@ package e2e_test import ( + "fmt" "strconv" "strings" @@ -23,6 +24,7 @@ type initMachine struct { timezone string rootful bool volumes []string + updateConnection *bool userModeNetworking bool tlsVerify *bool @@ -78,6 +80,10 @@ func (i *initMachine) buildCmd(m *machineTestBuilder) []string { if i.tlsVerify != nil { cmd = append(cmd, "--tls-verify="+strconv.FormatBool(*i.tlsVerify)) } + if i.updateConnection != nil { + cmd = append(cmd, fmt.Sprintf("--update-connection=%s", strconv.FormatBool(*i.updateConnection))) + } + name := m.name cmd = append(cmd, name) @@ -175,6 +181,11 @@ func (i *initMachine) withTlsVerify(tlsVerify *bool) *initMachine { return i } +func (i *initMachine) withUpdateConnection(value *bool) *initMachine { + i.updateConnection = value + return i +} + func (i *initMachine) withUserModeNetworking(r bool) *initMachine { //nolint:unused,nolintlint i.userModeNetworking = r return i diff --git a/pkg/machine/e2e/config_start_test.go b/pkg/machine/e2e/config_start_test.go index 66058e56133..904ab306550 100644 --- a/pkg/machine/e2e/config_start_test.go +++ b/pkg/machine/e2e/config_start_test.go @@ -1,11 +1,17 @@ package e2e_test +import ( + "fmt" + "strconv" +) + type startMachine struct { /* No command line args other than a machine vm name (also not required) */ - quiet bool - noInfo bool + quiet bool + noInfo bool + updateConnection *bool } func (s *startMachine) buildCmd(m *machineTestBuilder) []string { @@ -19,6 +25,9 @@ func (s *startMachine) buildCmd(m *machineTestBuilder) []string { if s.noInfo { cmd = append(cmd, "--no-info") } + if s.updateConnection != nil { + cmd = append(cmd, fmt.Sprintf("--update-connection=%s", strconv.FormatBool(*s.updateConnection))) + } return cmd } @@ -31,3 +40,12 @@ func (s *startMachine) withNoInfo() *startMachine { s.noInfo = true return s } + +func (s *startMachine) withUpdateConnection(value *bool) *startMachine { + s.updateConnection = value + return s +} + +func ptrBool(v bool) *bool { + return &v +} diff --git a/pkg/machine/e2e/rm_test.go b/pkg/machine/e2e/rm_test.go index b149a6a9bb9..48484924032 100644 --- a/pkg/machine/e2e/rm_test.go +++ b/pkg/machine/e2e/rm_test.go @@ -147,7 +147,7 @@ var _ = Describe("podman machine rm", func() { barName := "bar" bar := new(initMachine) - session, err = mb.setName(barName).setCmd(bar.withImage(mb.imagePath).withNow()).run() + session, err = mb.setName(barName).setCmd(bar.withUpdateConnection(ptrBool(false)).withImage(mb.imagePath).withNow()).run() Expect(err).ToNot(HaveOccurred()) Expect(session).To(Exit(0)) diff --git a/pkg/machine/e2e/start_test.go b/pkg/machine/e2e/start_test.go index e04e4a46a88..7799ec011e0 100644 --- a/pkg/machine/e2e/start_test.go +++ b/pkg/machine/e2e/start_test.go @@ -4,17 +4,18 @@ import ( "fmt" "net" "net/url" + "strconv" "sync" "time" "github.com/containers/podman/v6/pkg/machine/define" + jsoniter "github.com/json-iterator/go" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" . "github.com/onsi/gomega/gexec" ) var _ = Describe("podman machine start", func() { - It("start simple machine", func() { i := new(initMachine) session, err := mb.setCmd(i.withImage(mb.imagePath)).run() @@ -67,6 +68,7 @@ var _ = Describe("podman machine start", func() { session, err := machineTestBuilderInit.run() Expect(err).ToNot(HaveOccurred()) Expect(session).To(Exit(0)) + s := new(startMachine) startSession, err := mb.setCmd(s).run() Expect(err).ToNot(HaveOccurred()) @@ -184,7 +186,7 @@ var _ = Describe("podman machine start", func() { defer GinkgoRecover() defer wg.Done() s := &startMachine{} - startSession1, err = mb.setName(machine1).setCmd(s).setTimeout(time.Minute * 10).run() + startSession1, err = mb.setName(machine1).setCmd(s.withUpdateConnection(ptrBool(false))).setTimeout(time.Minute * 10).run() Expect(err).ToNot(HaveOccurred()) }() go func() { @@ -197,7 +199,7 @@ var _ = Describe("podman machine start", func() { // second run. nmb, err := newMB() Expect(err).ToNot(HaveOccurred()) - startSession2, err = nmb.setName(machine2).setCmd(s).setTimeout(time.Minute * 10).run() + startSession2, err = nmb.setName(machine2).setCmd(s.withUpdateConnection(ptrBool(false))).setTimeout(time.Minute * 10).run() Expect(err).ToNot(HaveOccurred()) }() wg.Wait() @@ -218,6 +220,89 @@ var _ = Describe("podman machine start", func() { Expect(startSession1.errorToString()).To(ContainSubstring("%s already starting or running: only one VM can be active at a time", machine2)) } }) + + It("machine start with --update-connection", func() { + // Add a connection and verify it was set to the default + defConnName := "QA" + err := addSystemConnection(defConnName, true) + Expect(err).ToNot(HaveOccurred()) + + listings, err := getSystemConnectionsAsSysConns() + Expect(err).ToNot(HaveOccurred()) + Expect(listings.IsDefault(defConnName)).To(BeTrue()) + + // Create a new machine + i := initMachine{} + machineName := randomString() + initSession, err := mb.setName(machineName).setCmd(i.withImage(mb.imagePath)).run() + Expect(err).ToNot(HaveOccurred()) + Expect(initSession).To(Exit(0)) + + // Start the new machine with --update-connection=false + s := startMachine{} + startSession, err := mb.setName(machineName).setCmd(s.withUpdateConnection(ptrBool(false))).run() + Expect(err).ToNot(HaveOccurred()) + Expect(startSession).To(Exit(0)) + + // We started the machine with --update-connection=false so it should not be default + listings, err = getSystemConnectionsAsSysConns() + Expect(err).ToNot(HaveOccurred()) + Expect(listings.IsDefault(defConnName)).To(BeTrue()) + + // Stop the machine + halt := stopMachine{} + stopSession, err := mb.setName(machineName).setCmd(halt).run() + Expect(err).ToNot(HaveOccurred()) + Expect(stopSession).To(Exit(0)) + + // Start the new machine with --update-connection + startSession, err = mb.setName(machineName).setCmd(s.withUpdateConnection(ptrBool(true))).run() + Expect(err).ToNot(HaveOccurred()) + Expect(startSession).To(Exit(0)) + + // We set true so the new default connection should have changed + listings, err = getSystemConnectionsAsSysConns() + Expect(err).ToNot(HaveOccurred()) + Expect(listings.IsDefault(machineName)).To(BeTrue()) + }) + It("machine init --now with --update-connection", func() { + // Add a connection and verify it was set to the default + defConnName := "QA" + err := addSystemConnection(defConnName, true) + Expect(err).ToNot(HaveOccurred()) + + listings, err := getSystemConnectionsAsSysConns() + Expect(err).ToNot(HaveOccurred()) + Expect(listings.IsDefault(defConnName)).To(BeTrue()) + + // Create a new machine + i := initMachine{} + machineName1 := randomString() + initSession, err := mb.setName(machineName1).setCmd(i.withImage(mb.imagePath).withUpdateConnection(ptrBool(false)).withNow()).run() + Expect(err).ToNot(HaveOccurred()) + Expect(initSession).To(Exit(0)) + + // We started the machine with --update-connection=false so it should not be default + listings, err = getSystemConnectionsAsSysConns() + Expect(err).ToNot(HaveOccurred()) + Expect(listings.IsDefault(defConnName)).To(BeTrue()) + + // Stop the machine + halt := stopMachine{} + stopSession, err := mb.setName(machineName1).setCmd(halt).run() + Expect(err).ToNot(HaveOccurred()) + Expect(stopSession).To(Exit(0)) + + // Create another machine + machineName2 := randomString() + initSession2, err := mb.setName(machineName2).setCmd(i.withImage(mb.imagePath).withUpdateConnection(ptrBool(true)).withNow()).run() + Expect(err).ToNot(HaveOccurred()) + Expect(initSession2).To(Exit(0)) + + listings, err = getSystemConnectionsAsSysConns() + Expect(err).ToNot(HaveOccurred()) + Expect(listings.IsDefault(machineName2)).To(BeTrue()) + }) }) func mapToPort(uris []string) ([]string, error) { @@ -238,3 +323,70 @@ func mapToPort(uris []string) ([]string, error) { } return ports, nil } + +func addSystemConnection(name string, setDefault bool) error { + addConn := []string{ + "system", "connection", "add", + fmt.Sprintf("--default=%s", strconv.FormatBool(setDefault)), + "--identity", "~/.ssh/id_rsa", + name, + "ssh://root@podman.test:2222/run/podman/podman.sock", + } + mb.cmd = addConn + addConnSession, err := mb.run() + if err != nil { + return err + } + if addConnSession.ExitCode() != 0 { + fmt.Println(addConnSession.outputToString()) + return fmt.Errorf("error: %s", addConnSession.errorToString()) + } + return nil +} + +func systemConnectionLsToSysConns(output []byte) (SysConns, error) { + var conns SysConns + err := jsoniter.Unmarshal(output, &conns) + return conns, err +} + +type SysConn struct { + Name string + URI string + Identity string + IsMachine bool + Default bool + ReadWrite bool +} + +type SysConns []SysConn + +func (s SysConns) IsDefault(name string) bool { + for _, conn := range s { + if conn.Name == name { + return conn.Default + } + } + return false +} + +func (s SysConns) GetDefault() (SysConn, error) { + for _, conn := range s { + if conn.Default { + return conn, nil + } + } + return SysConn{}, fmt.Errorf("no default connection found") +} + +func getSystemConnectionsAsSysConns() (SysConns, error) { + connections := new(listSystemConnection) + connSession, err := mb.setCmd(connections.withFormat("json")).run() + if err != nil { + return nil, err + } + if connSession.ExitCode() != 0 { + return nil, fmt.Errorf("error: %s", connSession.errorToString()) + } + return systemConnectionLsToSysConns(connSession.Out.Contents()) +} diff --git a/pkg/machine/os/machine_os.go b/pkg/machine/os/machine_os.go index 0f7b7177424..2ca5be5f1b2 100644 --- a/pkg/machine/os/machine_os.go +++ b/pkg/machine/os/machine_os.go @@ -21,6 +21,7 @@ type MachineOS struct { // Apply applies the image by sshing into the machine and running apply from inside the VM. func (m *MachineOS) Apply(image string, _ ApplyOptions) error { + var off bool args := []string{"podman", "machine", "os", "apply", image} if err := machine.LocalhostSSH(m.VM.SSH.RemoteUsername, m.VM.SSH.IdentityPath, m.VMName, m.VM.SSH.Port, args); err != nil { @@ -28,10 +29,10 @@ func (m *MachineOS) Apply(image string, _ ApplyOptions) error { } if m.Restart { - if err := shim.Stop(m.VM, m.Provider, false); err != nil { + if err := shim.Stop(m.VM, m.Provider, off); err != nil { return err } - if err := shim.Start(m.VM, m.Provider, machine.StartOptions{NoInfo: true}); err != nil { + if err := shim.Start(m.VM, m.Provider, machine.StartOptions{NoInfo: true}, &off); err != nil { return err } fmt.Printf("Machine %q restarted successfully\n", m.VMName) diff --git a/pkg/machine/shim/host.go b/pkg/machine/shim/host.go index 3d399e52fd2..e1fec6d664f 100644 --- a/pkg/machine/shim/host.go +++ b/pkg/machine/shim/host.go @@ -14,6 +14,7 @@ import ( "syscall" "time" + "github.com/containers/podman/v6/cmd/podman/registry" "github.com/containers/podman/v6/pkg/machine" "github.com/containers/podman/v6/pkg/machine/connection" machineDefine "github.com/containers/podman/v6/pkg/machine/define" @@ -27,6 +28,7 @@ import ( "github.com/containers/podman/v6/utils" "github.com/hashicorp/go-multierror" "github.com/sirupsen/logrus" + "go.podman.io/common/pkg/config" ) // List is done at the host level to allow for a *possible* future where @@ -171,7 +173,7 @@ func Init(opts machineDefine.InitOptions, mp vmconfigs.VMProvider) error { callbackFuncs.Add(mc.ImagePath.Delete) - logrus.Debugf("--> imagePath is %q", imagePath.GetPath()) + logrus.Debugf("imagePath is %q", imagePath.GetPath()) ignitionFile, err := mc.IgnitionFile() if err != nil { @@ -446,7 +448,9 @@ func stopLocked(mc *vmconfigs.MachineConfig, mp vmconfigs.VMProvider, dirs *mach return mc.Write() } -func Start(mc *vmconfigs.MachineConfig, mp vmconfigs.VMProvider, opts machine.StartOptions) error { +func Start(mc *vmconfigs.MachineConfig, mp vmconfigs.VMProvider, opts machine.StartOptions, updateSystemConn *bool) error { + var updateDefaultConnection bool + defaultBackoff := 500 * time.Millisecond maxBackoffs := 6 signalChanClosed := false @@ -461,6 +465,15 @@ func Start(mc *vmconfigs.MachineConfig, mp vmconfigs.VMProvider, opts machine.St return fmt.Errorf("reload config: %w", err) } + connName := mc.Name + if mc.HostUser.Rootful { + connName += "-root" + } + conn, err := registry.PodmanConfig().ContainersConfDefaultsRO.GetConnection(connName, false) + if err != nil { + return err + } + // Don't check if provider supports parallel running machines if mp.RequireExclusiveActive() { startLock, err := lock.GetMachineStartLock() @@ -485,6 +498,30 @@ func Start(mc *vmconfigs.MachineConfig, mp vmconfigs.VMProvider, opts machine.St } } + // Do not do anything with the system connection if its already + // the default system connection. + if !conn.Default { + // Prompt for system connection update + if updateSystemConn == nil { + response, err := promptUpdateSystemConn() + if err != nil { + return err + } + // This might be kind of lame but when using the command, but if you don't + // provide some sort of visual cue back to the user, it's unclear what is + // going on because the machine startup is going and it looks like things + // are frozen + if response { + fmt.Printf("\nDefault system connection will be changed to %q\n ", connName) + } else { + fmt.Println("Default system connection will remain unchanged") + } + updateDefaultConnection = response + } else { + updateDefaultConnection = *updateSystemConn + } + } + // if the machine cannot continue starting due to a signal, ensure the state // reflects the machine is no longer starting signalChan := make(chan os.Signal, 1) @@ -643,7 +680,16 @@ func Start(mc *vmconfigs.MachineConfig, mp vmconfigs.VMProvider, opts machine.St mc.HostUser.Rootful, ) - return nil + // return if we dont need to set the machine to the default connection + // or it was determined to already be the default earlier + if !updateDefaultConnection { + return nil + } + return config.EditConnectionConfig(func(cfg *config.ConnectionsFile) error { + logrus.Infof("Setting default Podman connection to %s", connName) + cfg.Connection.Default = connName + return nil + }) } func Set(mc *vmconfigs.MachineConfig, mp vmconfigs.VMProvider, opts machineDefine.SetOptions) error { @@ -849,3 +895,15 @@ func validateDestinationPaths(dest string) error { } return nil } + +func promptUpdateSystemConn() (bool, error) { + fmt.Println("Warning: The machine being started is not set as your default Podman connection.") + fmt.Println("As such, Podman commands may not work correctly.") + fmt.Print(`Set the default Podman connection to this machine? [y/N] `) + reader := bufio.NewReader(os.Stdin) + answer, err := reader.ReadString('\n') + if err != nil { + return false, err + } + return strings.ToLower(answer)[0] == 'y', nil +}