diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 00000000..aec7eb17 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,17 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Launch Package", + "type": "go", + "request": "launch", + "mode": "debug", + "buildFlags": [ + "-tags", + "containers_image_openpgp exclude_graphdriver_btrfs btrfs_noversion", + ], + "program": "${workspaceFolder}/cmd/macadam", + "args": ["init", "mac1"], + } + ] +} \ No newline at end of file diff --git a/cmd/macadam/init.go b/cmd/macadam/init.go new file mode 100644 index 00000000..cbb15ab9 --- /dev/null +++ b/cmd/macadam/init.go @@ -0,0 +1,183 @@ +//go:build amd64 || arm64 + +package main + +import ( + "fmt" + + "github.com/cfergeau/macadam/cmd/macadam/registry" + macadam "github.com/cfergeau/macadam/pkg/machinedriver" + "github.com/containers/common/pkg/completion" + ldefine "github.com/containers/podman/v5/libpod/define" + "github.com/containers/podman/v5/pkg/machine/define" + "github.com/containers/podman/v5/pkg/machine/env" + provider2 "github.com/containers/podman/v5/pkg/machine/provider" + "github.com/spf13/cobra" +) + +var ( + initCmd = &cobra.Command{ + Use: "init [options] [NAME]", + Short: "Initialize a virtual machine", + Long: "Initialize a virtual machine", + RunE: initMachine, + Args: cobra.MaximumNArgs(1), + Example: `macadam init podman-machine-default`, + ValidArgsFunction: completion.AutocompleteNone, + } + + initOpts = define.InitOptions{} + initOptionalFlags = InitOptionalFlags{} + defaultMachineName = define.DefaultMachineName + now bool +) + +// Flags which have a meaning when unspecified that differs from the flag default +type InitOptionalFlags struct { + UserModeNetworking bool +} + +// maxMachineNameSize is set to thirty to limit huge machine names primarily +// because macOS has a much smaller file size limit. +const maxMachineNameSize = 30 + +func init() { + registry.Commands = append(registry.Commands, registry.CliCommand{ + Command: initCmd, + }) + /* flags := initCmd.Flags() + cfg := registry.PodmanConfig() + + cpusFlagName := "cpus" + flags.Uint64Var( + &initOpts.CPUS, + cpusFlagName, cfg.ContainersConfDefaultsRO.Machine.CPUs, + "Number of CPUs", + ) + _ = initCmd.RegisterFlagCompletionFunc(cpusFlagName, completion.AutocompleteNone) + + diskSizeFlagName := "disk-size" + flags.Uint64Var( + &initOpts.DiskSize, + diskSizeFlagName, cfg.ContainersConfDefaultsRO.Machine.DiskSize, + "Disk size in GiB", + ) + + _ = initCmd.RegisterFlagCompletionFunc(diskSizeFlagName, completion.AutocompleteNone) + + memoryFlagName := "memory" + flags.Uint64VarP( + &initOpts.Memory, + memoryFlagName, "m", cfg.ContainersConfDefaultsRO.Machine.Memory, + "Memory in MiB", + ) + _ = initCmd.RegisterFlagCompletionFunc(memoryFlagName, completion.AutocompleteNone) + + flags.BoolVar( + &now, + "now", false, + "Start machine now", + ) + timezoneFlagName := "timezone" + defaultTz := cfg.ContainersConfDefaultsRO.TZ() + if len(defaultTz) < 1 { + defaultTz = "local" + } + flags.StringVar(&initOpts.TimeZone, timezoneFlagName, defaultTz, "Set timezone") + _ = initCmd.RegisterFlagCompletionFunc(timezoneFlagName, completion.AutocompleteDefault) + + flags.BoolVar( + &initOpts.ReExec, + "reexec", false, + "process was rexeced", + ) + _ = flags.MarkHidden("reexec") + + UsernameFlagName := "username" + flags.StringVar(&initOpts.Username, UsernameFlagName, cfg.ContainersConfDefaultsRO.Machine.User, "Username used in image") + _ = initCmd.RegisterFlagCompletionFunc(UsernameFlagName, completion.AutocompleteDefault) + + ImageFlagName := "image" + flags.StringVar(&initOpts.Image, ImageFlagName, cfg.ContainersConfDefaultsRO.Machine.Image, "Bootable image for machine") + _ = initCmd.RegisterFlagCompletionFunc(ImageFlagName, completion.AutocompleteDefault) + + // Deprecate image-path option, use --image instead + ImagePathFlagName := "image-path" + flags.StringVar(&initOpts.Image, ImagePathFlagName, cfg.ContainersConfDefaultsRO.Machine.Image, "Bootable image for machine") + _ = initCmd.RegisterFlagCompletionFunc(ImagePathFlagName, completion.AutocompleteDefault) + if err := flags.MarkDeprecated(ImagePathFlagName, "use --image instead"); err != nil { + logrus.Error("unable to mark image-path flag deprecated") + } + + VolumeFlagName := "volume" + flags.StringArrayVarP(&initOpts.Volumes, VolumeFlagName, "v", cfg.ContainersConfDefaultsRO.Machine.Volumes.Get(), "Volumes to mount, source:target") + _ = initCmd.RegisterFlagCompletionFunc(VolumeFlagName, completion.AutocompleteDefault) + + USBFlagName := "usb" + flags.StringArrayVarP(&initOpts.USBs, USBFlagName, "", []string{}, + "USB Host passthrough: bus=$1,devnum=$2 or vendor=$1,product=$2") + _ = initCmd.RegisterFlagCompletionFunc(USBFlagName, completion.AutocompleteDefault) + + VolumeDriverFlagName := "volume-driver" + flags.String(VolumeDriverFlagName, "", "Optional volume driver") + _ = initCmd.RegisterFlagCompletionFunc(VolumeDriverFlagName, completion.AutocompleteDefault) + if err := flags.MarkDeprecated(VolumeDriverFlagName, "will be ignored"); err != nil { + logrus.Error("unable to mark volume-driver flag deprecated") + } + + IgnitionPathFlagName := "ignition-path" + flags.StringVar(&initOpts.IgnitionPath, IgnitionPathFlagName, "", "Path to ignition file") + _ = initCmd.RegisterFlagCompletionFunc(IgnitionPathFlagName, completion.AutocompleteDefault) + + rootfulFlagName := "rootful" + flags.BoolVar(&initOpts.Rootful, rootfulFlagName, false, "Whether this machine should prefer rootful container execution") + + userModeNetFlagName := "user-mode-networking" + flags.BoolVar(&initOptionalFlags.UserModeNetworking, userModeNetFlagName, false, + "Whether this machine should use user-mode networking, routing traffic through a host user-space process") */ +} + +func initMachine(cmd *cobra.Command, args []string) error { + provider, err := provider2.Get() + if err != nil { + return err + } + + dirs, err := env.GetMachineDirs(provider.VMType()) + if err != nil { + return err + } + + machineName := defaultMachineName + if len(args) > 0 { + if len(args[0]) > maxMachineNameSize { + return fmt.Errorf("machine name %q must be %d characters or less", args[0], maxMachineNameSize) + } + machineName = args[0] + + if !ldefine.NameRegex.MatchString(machineName) { + return fmt.Errorf("invalid name %q: %w", machineName, ldefine.RegexError) + } + } + + sshIdentityPath, err := env.GetSSHIdentityPath(define.DefaultIdentityName) + if err != nil { + return err + } + + driver := macadam.NewDriver(machineName, dirs.ConfigDir.Path, sshIdentityPath) + + /* sshPort, err := ports.AllocateMachinePort() + if err != nil { + return err + } + + sshConfig := drivers.SSHConfig{ + IdentityPath: sshIdentityPath, + Port: sshPort, + RemoteUsername: "username", //todo user can provide it + } + driver.UpdateSSHConfig(sshConfig) */ + + return driver.Create() +} diff --git a/cmd/macadam/main.go b/cmd/macadam/main.go index ad7a1a59..01462531 100644 --- a/cmd/macadam/main.go +++ b/cmd/macadam/main.go @@ -1,201 +1,48 @@ package main import ( - "errors" "fmt" - "log/slog" "os" - "github.com/cfergeau/macadam/pkg/cmdline" + "github.com/cfergeau/macadam/cmd/macadam/registry" - "github.com/containers/common/pkg/config" - ldefine "github.com/containers/podman/v5/libpod/define" - - "github.com/containers/podman/v5/pkg/machine" - "github.com/containers/podman/v5/pkg/machine/define" - "github.com/containers/podman/v5/pkg/machine/env" - provider2 "github.com/containers/podman/v5/pkg/machine/provider" - "github.com/containers/podman/v5/pkg/machine/shim" - "github.com/containers/podman/v5/pkg/machine/vmconfigs" - - _ "github.com/spf13/cobra" + "github.com/spf13/cobra" ) -var ( - initOpts = define.InitOptions{} - defaultMachineName = define.DefaultMachineName -) +func main() { + rootCmd = parseCommands() -type PodmanMachine struct { - provider vmconfigs.VMProvider - config *vmconfigs.MachineConfig + Execute() + os.Exit(0) } -func startMachine(m *PodmanMachine) error { - - machineName := m.config.Name - dirs, err := env.GetMachineDirs(m.provider.VMType()) - if err != nil { - return err +func parseCommands() *cobra.Command { + for _, c := range registry.Commands { + addCommand(c) } - /* - mc, err := vmconfigs.LoadMachineByName(machineName, dirs) - if err != nil { - return err - } - */ - fmt.Printf("Starting machine %q\n", machineName) - - startOpts := machine.StartOptions{ - NoInfo: false, - Quiet: false, - } - slog.Info(fmt.Sprintf("SSH config: %v", m.config.SSH)) - - if err := shim.Start(m.config, m.provider, dirs, startOpts); err != nil { - return err - } - fmt.Printf("Machine %q started successfully\n", machineName) - //newMachineEvent(events.Start, events.Event{Name: vmName}) - return nil + rootCmd.SetFlagErrorFunc(flagErrorFuncfunc) + return rootCmd } -func initMachine(initOpts define.InitOptions) (*PodmanMachine, error) { - machine := PodmanMachine{} - provider, err := provider2.Get() - if err != nil { - return nil, err - } - machine.provider = provider - - // The vmtype names need to be reserved and cannot be used for podman machine names - if _, err := define.ParseVMType(initOpts.Name, define.UnknownVirt); err == nil { - return nil, fmt.Errorf("cannot use %q for a machine name", initOpts.Name) - } - - if !ldefine.NameRegex.MatchString(initOpts.Username) { - return nil, fmt.Errorf("invalid username %q: %w", initOpts.Username, ldefine.RegexError) - } - - // Check if machine already exists - vmConfig, exists, err := shim.VMExists(initOpts.Name, []vmconfigs.VMProvider{provider}) - if err != nil { - return nil, err - } - machine.config = vmConfig - - // machine exists, return error - if exists { - return &machine, fmt.Errorf("%s: %w", initOpts.Name, define.ErrVMAlreadyExists) - } - - /* - // check if a system connection already exists - cons, err := registry.PodmanConfig().ContainersConfDefaultsRO.GetAllConnections() - if err != nil { - return err - } - for _, con := range cons { - if con.ReadWrite { - for _, connection := range []string{initOpts.Name, fmt.Sprintf("%s-root", initOpts.Name)} { - if con.Name == connection { - return fmt.Errorf("system connection %q already exists. consider a different machine name or remove the connection with `podman system connection rm`", connection) - } - } - } - } - */ - - for idx, vol := range initOpts.Volumes { - initOpts.Volumes[idx] = os.ExpandEnv(vol) - } - - // TODO need to work this back in - // 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 - // // exit gracefully with no error and no success message. - // // Examples: - // // - 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 - // } - - err = shim.Init(initOpts, provider) - if err != nil { - return nil, err - } - - /* - newMachineEvent(events.Init, events.Event{Name: initOpts.Name}) - */ - fmt.Println("Machine init complete") - - vmConfig, _, err = shim.VMExists(initOpts.Name, []vmconfigs.VMProvider{provider}) - if err != nil { - return nil, err - } - machine.config = vmConfig - - /* - now := false - if now { - return startMachine(initOpts.Name, provider) - } - */ - extra := "" - - if initOpts.Name != defaultMachineName { - extra = " " + initOpts.Name - } - fmt.Printf("To start your machine run:\n\n\tpodman machine start%s\n\n", extra) - return &machine, err +func flagErrorFuncfunc(c *cobra.Command, e error) error { + e = fmt.Errorf("%w\nSee '%s --help'", e, c.CommandPath()) + return e } -func main() { - slog.Info(fmt.Sprintf("macadam version %s", cmdline.Version())) - - defaultConfig, err := config.New(&config.Options{ - SetDefault: true, // This makes sure that following calls to config.Default() return this config - }) - if err != nil { - os.Exit(1) +func addCommand(c registry.CliCommand) { + parent := rootCmd + if c.Parent != nil { + parent = c.Parent } + parent.AddCommand(c.Command) - // defaults from cmd/podman/machine/init.go - initOpts.Name = defaultMachineName + c.Command.SetFlagErrorFunc(flagErrorFuncfunc) - initOpts.CPUS = defaultConfig.Machine.CPUs - initOpts.DiskSize = defaultConfig.Machine.DiskSize - initOpts.Memory = defaultConfig.Machine.Memory - defaultTz := defaultConfig.TZ() - if len(defaultTz) < 1 { - defaultTz = "local" - } - initOpts.TimeZone = defaultTz - initOpts.ReExec = false - initOpts.Username = defaultConfig.Machine.User - initOpts.Image = defaultConfig.Machine.Image - initOpts.Volumes = defaultConfig.Machine.Volumes.Get() - initOpts.USBs = []string{} - initOpts.IgnitionPath = "" - initOpts.Rootful = false - userModeNetworking := false - initOpts.UserModeNetworking = &userModeNetworking - // user-mode networking - - machine, err := initMachine(initOpts) - if err != nil && !errors.Is(err, define.ErrVMAlreadyExists) { - slog.Error(err.Error()) - os.Exit(1) - } - if err := startMachine(machine); err != nil { - slog.Error(err.Error()) - } - /* - if err != nil || errors.Is(err, define.ErrVMAlreadyExists) - { - */ + // - templates need to be set here, as PersistentPreRunE() is + // not called when --help is used. + // - rootCmd uses cobra default template not ours + c.Command.SetHelpTemplate(helpTemplate) + c.Command.SetUsageTemplate(usageTemplate) + c.Command.DisableFlagsInUseLine = true } diff --git a/cmd/macadam/registry/registry.go b/cmd/macadam/registry/registry.go new file mode 100644 index 00000000..e25acb59 --- /dev/null +++ b/cmd/macadam/registry/registry.go @@ -0,0 +1,26 @@ +package registry + +import ( + "github.com/spf13/cobra" +) + +type CliCommand struct { + Command *cobra.Command + Parent *cobra.Command +} + +var ( + exitCode = 0 + + // Commands holds the cobra.Commands to present to the user, including + // parent if not a child of "root" + Commands []CliCommand +) + +func SetExitCode(code int) { + exitCode = code +} + +func GetExitCode() int { + return exitCode +} diff --git a/cmd/macadam/root.go b/cmd/macadam/root.go new file mode 100644 index 00000000..70290d8f --- /dev/null +++ b/cmd/macadam/root.go @@ -0,0 +1,84 @@ +package main + +import ( + "context" + "fmt" + "os" + "path/filepath" + + "github.com/cfergeau/macadam/cmd/macadam/registry" + "github.com/cfergeau/macadam/pkg/cmdline" + "github.com/containers/podman/v5/libpod/define" + "github.com/spf13/cobra" +) + +// HelpTemplate is the help template for podman commands +// This uses the short and long options. +// command should not use this. +const helpTemplate = `{{.Short}} + +Description: + {{.Long}} + +{{if or .Runnable .HasSubCommands}}{{.UsageString}}{{end}}` + +// UsageTemplate is the usage template for podman commands +// This blocks the displaying of the global options. The main podman +// command should not use this. +const usageTemplate = `Usage:{{if (and .Runnable (not .HasAvailableSubCommands))}} + {{.UseLine}}{{end}}{{if .HasAvailableSubCommands}} + {{.UseLine}} [command]{{end}}{{if gt (len .Aliases) 0}} + +Aliases: + {{.NameAndAliases}}{{end}}{{if .HasExample}} + +Examples: + {{.Example}}{{end}}{{if .HasAvailableSubCommands}} + +Available Commands:{{range .Commands}}{{if (or .IsAvailableCommand (eq .Name "help"))}} + {{rpad .Name .NamePadding }} {{.Short}}{{end}}{{end}}{{end}}{{if .HasAvailableLocalFlags}} + +Options: +{{.LocalFlags.FlagUsages | trimTrailingWhitespaces}}{{end}}{{if .HasAvailableInheritedFlags}} +{{end}} +` + +var ( + rootCmd = &cobra.Command{ + Use: filepath.Base(os.Args[0]) + " [options]", + Long: "Manage pods, containers and images", + SilenceUsage: true, + SilenceErrors: true, + TraverseChildren: true, + Version: cmdline.Version(), + DisableFlagsInUseLine: true, + } + + defaultLogLevel = "warn" + logLevel = defaultLogLevel + dockerConfig = "" + debug bool + + requireCleanup = true + + // Defaults for capturing/redirecting the command output since (the) cobra is + // global-hungry and doesn't allow you to attach anything that allows us to + // transform the noStdout BoolVar to a string that we can assign to useStdout. + noStdout = false + useStdout = "" +) + +func init() { + rootCmd.SetUsageTemplate(usageTemplate) +} + +func Execute() { + if err := rootCmd.ExecuteContext(context.Background()); err != nil { + if registry.GetExitCode() == 0 { + registry.SetExitCode(define.ExecErrorCodeGeneric) + } + fmt.Fprintln(os.Stderr, err) + } + + os.Exit(registry.GetExitCode()) +} diff --git a/cmd/macadam/start.go b/cmd/macadam/start.go new file mode 100644 index 00000000..a0d8de45 --- /dev/null +++ b/cmd/macadam/start.go @@ -0,0 +1,69 @@ +//go:build amd64 || arm64 + +package main + +import ( + "fmt" + + "github.com/cfergeau/macadam/cmd/macadam/registry" + macadam "github.com/cfergeau/macadam/pkg/machinedriver" + ldefine "github.com/containers/podman/v5/libpod/define" + "github.com/containers/podman/v5/pkg/machine" + "github.com/containers/podman/v5/pkg/machine/env" + "github.com/spf13/cobra" + + provider2 "github.com/containers/podman/v5/pkg/machine/provider" +) + +var ( + startCmd = &cobra.Command{ + Use: "start [options] [MACHINE]", + Short: "Start an existing machine", + Long: "Start a managed virtual machine ", + RunE: start, + Args: cobra.MaximumNArgs(1), + Example: `macadam start podman-machine-default`, + } + startOpts = machine.StartOptions{} +) + +func init() { + registry.Commands = append(registry.Commands, registry.CliCommand{ + Command: startCmd, + }) + + flags := startCmd.Flags() + noInfoFlagName := "no-info" + flags.BoolVar(&startOpts.NoInfo, noInfoFlagName, false, "Suppress informational tips") + + quietFlagName := "quiet" + flags.BoolVarP(&startOpts.Quiet, quietFlagName, "q", false, "Suppress machine starting status output") +} + +func start(_ *cobra.Command, args []string) error { + provider, err := provider2.Get() + if err != nil { + return err + } + + dirs, err := env.GetMachineDirs(provider.VMType()) + if err != nil { + return err + } + + machineName := defaultMachineName + if len(args) > 0 { + if len(args[0]) > maxMachineNameSize { + return fmt.Errorf("machine name %q must be %d characters or less", args[0], maxMachineNameSize) + } + machineName = args[0] + + if !ldefine.NameRegex.MatchString(initOpts.Name) { + return fmt.Errorf("invalid name %q: %w", initOpts.Name, ldefine.RegexError) + } + } + driver := macadam.NewDriver(machineName, dirs.ConfigDir.Path, "") + + // we cannot start the start command if it was not init immediately before + return driver.Start() +} diff --git a/pkg/machinedriver/driver.go b/pkg/machinedriver/driver.go index 1035a87b..e7416ec5 100644 --- a/pkg/machinedriver/driver.go +++ b/pkg/machinedriver/driver.go @@ -60,7 +60,7 @@ type Driver struct { vmProvider vmconfigs.VMProvider } -func NewDriver(hostName, storePath string) *Driver { +func NewDriver(hostName, storePath, sshIdentityPath string) *Driver { // checks that macdriver.Driver implements the libmachine.Driver interface var _ drivers.Driver = &Driver{} @@ -81,6 +81,11 @@ func NewDriver(hostName, storePath string) *Driver { // DaemonVsockPort was introduced DaemonVsockPort: DaemonVsockPort, + vmConfig: &vmconfigs.MachineConfig{ + SSH: vmconfigs.SSHConfig{ + IdentityPath: sshIdentityPath, + }, + }, vmProvider: provider, } } @@ -132,9 +137,9 @@ func (d *Driver) initOpts() *define.InitOptions { initOpts.Volumes = defaultConfig.Machine.Volumes.Get() */ initOpts.Username = "core" - initOpts.SSHIdentityPath = d.VMDriver.SSHConfig.IdentityPath - if d.VMDriver.SSHConfig.RemoteUsername != "" { - initOpts.Username = d.VMDriver.SSHConfig.RemoteUsername + initOpts.SSHIdentityPath = d.vmConfig.SSH.IdentityPath + if d.vmConfig.SSH.RemoteUsername != "" { + initOpts.Username = d.vmConfig.SSH.RemoteUsername } initOpts.Image = d.getDiskPath() initOpts.Volumes = []string{}