diff options
Diffstat (limited to 'cmd')
33 files changed, 893 insertions, 364 deletions
diff --git a/cmd/podman/common/completion.go b/cmd/podman/common/completion.go index cb3efe592..f1dea4113 100644 --- a/cmd/podman/common/completion.go +++ b/cmd/podman/common/completion.go @@ -13,7 +13,6 @@ import ( "github.com/containers/podman/v3/libpod/define" "github.com/containers/podman/v3/libpod/network/types" "github.com/containers/podman/v3/pkg/domain/entities" - "github.com/containers/podman/v3/pkg/network" "github.com/containers/podman/v3/pkg/rootless" systemdDefine "github.com/containers/podman/v3/pkg/systemd/define" "github.com/containers/podman/v3/pkg/util" @@ -209,7 +208,7 @@ func getImages(cmd *cobra.Command, toComplete string) ([]string, cobra.ShellComp return suggestions, cobra.ShellCompDirectiveNoFileComp } -func getSecrets(cmd *cobra.Command, toComplete string) ([]string, cobra.ShellCompDirective) { +func getSecrets(cmd *cobra.Command, toComplete string, cType completeType) ([]string, cobra.ShellCompDirective) { suggestions := []string{} engine, err := setupContainerEngine(cmd) @@ -224,7 +223,13 @@ func getSecrets(cmd *cobra.Command, toComplete string) ([]string, cobra.ShellCom } for _, s := range secrets { - if strings.HasPrefix(s.Spec.Name, toComplete) { + // works the same as in getNetworks + if ((len(toComplete) > 1 && cType == completeDefault) || + cType == completeIDs) && strings.HasPrefix(s.ID, toComplete) { + suggestions = append(suggestions, s.ID[0:12]) + } + // include name in suggestions + if cType != completeIDs && strings.HasPrefix(s.Spec.Name, toComplete) { suggestions = append(suggestions, s.Spec.Name) } } @@ -256,12 +261,11 @@ func getNetworks(cmd *cobra.Command, toComplete string, cType completeType) ([]s } for _, n := range networks { - id := network.GetNetworkID(n.Name) // include ids in suggestions if cType == completeIDs or // more then 2 chars are typed and cType == completeDefault if ((len(toComplete) > 1 && cType == completeDefault) || - cType == completeIDs) && strings.HasPrefix(id, toComplete) { - suggestions = append(suggestions, id[0:12]) + cType == completeIDs) && strings.HasPrefix(n.ID, toComplete) { + suggestions = append(suggestions, n.ID[0:12]) } // include name in suggestions if cType != completeIDs && strings.HasPrefix(n.Name, toComplete) { @@ -470,7 +474,7 @@ func AutocompleteSecrets(cmd *cobra.Command, args []string, toComplete string) ( if !validCurrentCmdLine(cmd, args, toComplete) { return nil, cobra.ShellCompDirectiveNoFileComp } - return getSecrets(cmd, toComplete) + return getSecrets(cmd, toComplete, completeDefault) } func AutocompleteSecretCreate(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { @@ -1283,6 +1287,15 @@ func AutocompleteVolumeFilters(cmd *cobra.Command, args []string, toComplete str return completeKeyValues(toComplete, kv) } +// AutocompleteSecretFilters - Autocomplete secret ls --filter options. +func AutocompleteSecretFilters(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + kv := keyValueCompletion{ + "name=": func(s string) ([]string, cobra.ShellCompDirective) { return getSecrets(cmd, s, completeNames) }, + "id=": func(s string) ([]string, cobra.ShellCompDirective) { return getSecrets(cmd, s, completeIDs) }, + } + return completeKeyValues(toComplete, kv) +} + // AutocompleteCheckpointCompressType - Autocomplete checkpoint compress type options. // -> "gzip", "none", "zstd" func AutocompleteCheckpointCompressType(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { diff --git a/cmd/podman/common/create.go b/cmd/podman/common/create.go index dad79348d..32d227e65 100644 --- a/cmd/podman/common/create.go +++ b/cmd/podman/common/create.go @@ -292,6 +292,14 @@ func DefineCreateFlags(cmd *cobra.Command, cf *entities.ContainerCreateOptions, "Set proxy environment variables in the container based on the host proxy vars", ) + hostUserFlagName := "hostuser" + createFlags.StringSliceVar( + &cf.HostUsers, + hostUserFlagName, []string{}, + "Host user account to add to /etc/passwd within container", + ) + _ = cmd.RegisterFlagCompletionFunc(hostUserFlagName, completion.AutocompleteNone) + imageVolumeFlagName := "image-volume" createFlags.StringVar( &cf.ImageVolume, @@ -327,13 +335,10 @@ func DefineCreateFlags(cmd *cobra.Command, cf *entities.ContainerCreateOptions, ) _ = cmd.RegisterFlagCompletionFunc(ipcFlagName, AutocompleteNamespace) - kernelMemoryFlagName := "kernel-memory" - createFlags.StringVar( - &cf.KernelMemory, - kernelMemoryFlagName, "", - "Kernel memory limit "+sizeWithUnitFormat, + createFlags.String( + "kernel-memory", "", + "DEPRECATED: Option is just hear for compatibility with Docker", ) - _ = cmd.RegisterFlagCompletionFunc(kernelMemoryFlagName, completion.AutocompleteNone) // kernel-memory is deprecated in the runtime spec. _ = createFlags.MarkHidden("kernel-memory") @@ -535,14 +540,6 @@ func DefineCreateFlags(cmd *cobra.Command, cf *entities.ContainerCreateOptions, ) _ = cmd.RegisterFlagCompletionFunc(secretFlagName, AutocompleteSecrets) - securityOptFlagName := "security-opt" - createFlags.StringArrayVar( - &cf.SecurityOpt, - securityOptFlagName, []string{}, - "Security Options", - ) - _ = cmd.RegisterFlagCompletionFunc(securityOptFlagName, AutocompleteSecurityOption) - shmSizeFlagName := "shm-size" createFlags.String( shmSizeFlagName, shmSize(), @@ -715,6 +712,13 @@ func DefineCreateFlags(cmd *cobra.Command, cf *entities.ContainerCreateOptions, `If a container with the same name exists, replace it`, ) } + securityOptFlagName := "security-opt" + createFlags.StringArrayVar( + &cf.SecurityOpt, + securityOptFlagName, []string{}, + "Security Options", + ) + _ = cmd.RegisterFlagCompletionFunc(securityOptFlagName, AutocompleteSecurityOption) subgidnameFlagName := "subgidname" createFlags.StringVar( @@ -885,6 +889,7 @@ func DefineCreateFlags(cmd *cobra.Command, cf *entities.ContainerCreateOptions, "Limit read rate (bytes per second) from a device (e.g. --device-read-bps=/dev/sda:1mb)", ) _ = cmd.RegisterFlagCompletionFunc(deviceReadBpsFlagName, completion.AutocompleteDefault) + volumesFromFlagName := "volumes-from" createFlags.StringArrayVar( &cf.VolumesFrom, diff --git a/cmd/podman/common/create_opts.go b/cmd/podman/common/create_opts.go index 7d6471fd4..f2335a2be 100644 --- a/cmd/podman/common/create_opts.go +++ b/cmd/podman/common/create_opts.go @@ -18,7 +18,6 @@ import ( "github.com/containers/podman/v3/pkg/specgen" "github.com/docker/docker/api/types/mount" "github.com/pkg/errors" - "github.com/sirupsen/logrus" ) func stringMaptoArray(m map[string]string) []string { @@ -156,18 +155,11 @@ func ContainerCreateToContainerCLIOpts(cc handlers.CreateContainerConfig, rtc *c } // netMode - nsmode, networks, err := specgen.ParseNetworkNamespace(string(cc.HostConfig.NetworkMode), true) + nsmode, networks, netOpts, err := specgen.ParseNetworkFlag([]string{string(cc.HostConfig.NetworkMode)}) if err != nil { return nil, nil, err } - var netOpts map[string][]string - parts := strings.SplitN(string(cc.HostConfig.NetworkMode), ":", 2) - if len(parts) > 1 { - netOpts = make(map[string][]string) - netOpts[parts[0]] = strings.Split(parts[1], ",") - } - // network // Note: we cannot emulate compat exactly here. we only allow specifics of networks to be // defined when there is only one network. @@ -184,52 +176,56 @@ func ContainerCreateToContainerCLIOpts(cc handlers.CreateContainerConfig, rtc *c // network names switch { case len(cc.NetworkingConfig.EndpointsConfig) > 0: - var aliases []string - endpointsConfig := cc.NetworkingConfig.EndpointsConfig - cniNetworks := make([]string, 0, len(endpointsConfig)) + networks := make(map[string]types.PerNetworkOptions, len(endpointsConfig)) for netName, endpoint := range endpointsConfig { - cniNetworks = append(cniNetworks, netName) - - if endpoint == nil { - continue - } - if len(endpoint.Aliases) > 0 { - aliases = append(aliases, endpoint.Aliases...) - } - } + netOpts := types.PerNetworkOptions{} + if endpoint != nil { + netOpts.Aliases = endpoint.Aliases - // static IP and MAC - if len(endpointsConfig) == 1 { - for _, ep := range endpointsConfig { - if ep == nil { - continue - } // if IP address is provided - if len(ep.IPAddress) > 0 { - staticIP := net.ParseIP(ep.IPAddress) - netInfo.StaticIP = &staticIP + if len(endpoint.IPAddress) > 0 { + staticIP := net.ParseIP(endpoint.IPAddress) + if staticIP == nil { + return nil, nil, errors.Errorf("failed to parse the ip address %q", endpoint.IPAddress) + } + netOpts.StaticIPs = append(netOpts.StaticIPs, staticIP) } - // if IPAMConfig.IPv4Address is provided - if ep.IPAMConfig != nil && ep.IPAMConfig.IPv4Address != "" { - staticIP := net.ParseIP(ep.IPAMConfig.IPv4Address) - netInfo.StaticIP = &staticIP + + if endpoint.IPAMConfig != nil { + // if IPAMConfig.IPv4Address is provided + if len(endpoint.IPAMConfig.IPv4Address) > 0 { + staticIP := net.ParseIP(endpoint.IPAMConfig.IPv4Address) + if staticIP == nil { + return nil, nil, errors.Errorf("failed to parse the ipv4 address %q", endpoint.IPAMConfig.IPv4Address) + } + netOpts.StaticIPs = append(netOpts.StaticIPs, staticIP) + } + // if IPAMConfig.IPv6Address is provided + if len(endpoint.IPAMConfig.IPv6Address) > 0 { + staticIP := net.ParseIP(endpoint.IPAMConfig.IPv6Address) + if staticIP == nil { + return nil, nil, errors.Errorf("failed to parse the ipv6 address %q", endpoint.IPAMConfig.IPv6Address) + } + netOpts.StaticIPs = append(netOpts.StaticIPs, staticIP) + } } // If MAC address is provided - if len(ep.MacAddress) > 0 { - staticMac, err := net.ParseMAC(ep.MacAddress) + if len(endpoint.MacAddress) > 0 { + staticMac, err := net.ParseMAC(endpoint.MacAddress) if err != nil { - return nil, nil, err + return nil, nil, errors.Errorf("failed to parse the mac address %q", endpoint.MacAddress) } - netInfo.StaticMAC = &staticMac + netOpts.StaticMAC = types.HardwareAddr(staticMac) } - break } + + networks[netName] = netOpts } - netInfo.Aliases = aliases - netInfo.CNINetworks = cniNetworks + + netInfo.Networks = networks case len(cc.HostConfig.NetworkMode) > 0: - netInfo.CNINetworks = networks + netInfo.Networks = networks } parsedTmp := make([]string, 0, len(cc.HostConfig.Tmpfs)) @@ -388,9 +384,6 @@ func ContainerCreateToContainerCLIOpts(cc handlers.CreateContainerConfig, rtc *c if cc.HostConfig.Memory > 0 { cliOpts.Memory = strconv.Itoa(int(cc.HostConfig.Memory)) } - if cc.HostConfig.KernelMemory > 0 { - logrus.Warnf("The --kernel-memory flag has been deprecated. May not work properly on your system.") - } if cc.HostConfig.MemoryReservation > 0 { cliOpts.MemoryReservation = strconv.Itoa(int(cc.HostConfig.MemoryReservation)) @@ -412,9 +405,6 @@ func ContainerCreateToContainerCLIOpts(cc handlers.CreateContainerConfig, rtc *c cliOpts.ShmSize = strconv.Itoa(int(cc.HostConfig.ShmSize)) } - if cc.HostConfig.KernelMemory > 0 { - cliOpts.KernelMemory = strconv.Itoa(int(cc.HostConfig.KernelMemory)) - } if len(cc.HostConfig.RestartPolicy.Name) > 0 { policy := cc.HostConfig.RestartPolicy.Name // only add restart count on failure diff --git a/cmd/podman/common/create_test.go b/cmd/podman/common/create_test.go index 17b47dd16..601078b61 100644 --- a/cmd/podman/common/create_test.go +++ b/cmd/podman/common/create_test.go @@ -12,7 +12,7 @@ import ( func TestPodOptions(t *testing.T) { entry := "/test1" - exampleOptions := entities.ContainerCreateOptions{CPUS: 5.5, CPUSetCPUs: "0-4", Entrypoint: &entry, Hostname: "foo", Name: "testing123", Volume: []string{"/fakeVol1", "/fakeVol2"}, Net: &entities.NetOptions{CNINetworks: []string{"FakeNetwork"}}, PID: "ns:/proc/self/ns"} + exampleOptions := entities.ContainerCreateOptions{CPUS: 5.5, CPUSetCPUs: "0-4", Entrypoint: &entry, Hostname: "foo", Name: "testing123", Volume: []string{"/fakeVol1", "/fakeVol2"}, Net: &entities.NetOptions{DNSSearch: []string{"search"}}, PID: "ns:/proc/self/ns"} podOptions := entities.PodCreateOptions{} err := common.ContainerToPodOptions(&exampleOptions, &podOptions) diff --git a/cmd/podman/common/netflags.go b/cmd/podman/common/netflags.go index d11f3c9d2..425d85c9d 100644 --- a/cmd/podman/common/netflags.go +++ b/cmd/podman/common/netflags.go @@ -6,6 +6,7 @@ import ( "github.com/containers/common/pkg/completion" "github.com/containers/podman/v3/cmd/podman/parse" "github.com/containers/podman/v3/libpod/define" + "github.com/containers/podman/v3/libpod/network/types" "github.com/containers/podman/v3/pkg/domain/entities" "github.com/containers/podman/v3/pkg/specgen" "github.com/containers/podman/v3/pkg/specgenutil" @@ -52,6 +53,13 @@ func DefineNetFlags(cmd *cobra.Command) { ) _ = cmd.RegisterFlagCompletionFunc(ipFlagName, completion.AutocompleteNone) + ip6FlagName := "ip6" + netFlags.String( + ip6FlagName, "", + "Specify a static IPv6 address for the container", + ) + _ = cmd.RegisterFlagCompletionFunc(ip6FlagName, completion.AutocompleteNone) + macAddressFlagName := "mac-address" netFlags.String( macAddressFlagName, "", @@ -60,8 +68,8 @@ func DefineNetFlags(cmd *cobra.Command) { _ = cmd.RegisterFlagCompletionFunc(macAddressFlagName, completion.AutocompleteNone) networkFlagName := "network" - netFlags.String( - networkFlagName, containerConfig.NetNS(), + netFlags.StringArray( + networkFlagName, nil, "Connect a container to a network", ) _ = cmd.RegisterFlagCompletionFunc(networkFlagName, AutocompleteNetworkFlag) @@ -87,9 +95,7 @@ func DefineNetFlags(cmd *cobra.Command) { } // NetFlagsToNetOptions parses the network flags for the given cmd. -// The netnsFromConfig bool is used to indicate if the --network flag -// should always be parsed regardless if it was set on the cli. -func NetFlagsToNetOptions(opts *entities.NetOptions, flags pflag.FlagSet, netnsFromConfig bool) (*entities.NetOptions, error) { +func NetFlagsToNetOptions(opts *entities.NetOptions, flags pflag.FlagSet) (*entities.NetOptions, error) { var ( err error ) @@ -151,18 +157,6 @@ func NetFlagsToNetOptions(opts *entities.NetOptions, flags pflag.FlagSet, netnsF } opts.DNSSearch = dnsSearches - m, err := flags.GetString("mac-address") - if err != nil { - return nil, err - } - if len(m) > 0 { - mac, err := net.ParseMAC(m) - if err != nil { - return nil, err - } - opts.StaticMAC = &mac - } - inputPorts, err := flags.GetStringSlice("publish") if err != nil { return nil, err @@ -174,52 +168,108 @@ func NetFlagsToNetOptions(opts *entities.NetOptions, flags pflag.FlagSet, netnsF } } - ip, err := flags.GetString("ip") - if err != nil { - return nil, err - } - if ip != "" { - staticIP := net.ParseIP(ip) - if staticIP == nil { - return nil, errors.Errorf("%s is not an ip address", ip) - } - if staticIP.To4() == nil { - return nil, errors.Wrapf(define.ErrInvalidArg, "%s is not an IPv4 address", ip) - } - opts.StaticIP = &staticIP - } - opts.NoHosts, err = flags.GetBool("no-hosts") if err != nil { return nil, err } - // parse the --network value only when the flag is set or we need to use - // the netns config value, e.g. when --pod is not used - if netnsFromConfig || flags.Changed("network") { - network, err := flags.GetString("network") + // parse the network only when network was changed + // otherwise we send default to server so that the server + // can pick the correct default instead of the client + if flags.Changed("network") { + network, err := flags.GetStringArray("network") if err != nil { return nil, err } - ns, cniNets, options, err := specgen.ParseNetworkString(network) + ns, networks, options, err := specgen.ParseNetworkFlag(network) if err != nil { return nil, err } - if len(options) > 0 { - opts.NetworkOptions = options - } + opts.NetworkOptions = options opts.Network = ns - opts.CNINetworks = cniNets + opts.Networks = networks } - aliases, err := flags.GetStringSlice("network-alias") - if err != nil { - return nil, err - } - if len(aliases) > 0 { - opts.Aliases = aliases + if flags.Changed("ip") || flags.Changed("ip6") || flags.Changed("mac-address") || flags.Changed("network-alias") { + // if there is no network we add the default + if len(opts.Networks) == 0 { + opts.Networks = map[string]types.PerNetworkOptions{ + "default": {}, + } + } + + for _, ipFlagName := range []string{"ip", "ip6"} { + ip, err := flags.GetString(ipFlagName) + if err != nil { + return nil, err + } + if ip != "" { + // if pod create --infra=false + if infra, err := flags.GetBool("infra"); err == nil && !infra { + return nil, errors.Wrapf(define.ErrInvalidArg, "cannot set --%s without infra container", ipFlagName) + } + + staticIP := net.ParseIP(ip) + if staticIP == nil { + return nil, errors.Errorf("%q is not an ip address", ip) + } + if !opts.Network.IsBridge() && !opts.Network.IsDefault() { + return nil, errors.Wrapf(define.ErrInvalidArg, "--%s can only be set when the network mode is bridge", ipFlagName) + } + if len(opts.Networks) != 1 { + return nil, errors.Wrapf(define.ErrInvalidArg, "--%s can only be set for a single network", ipFlagName) + } + for name, netOpts := range opts.Networks { + netOpts.StaticIPs = append(netOpts.StaticIPs, staticIP) + opts.Networks[name] = netOpts + } + } + } + + m, err := flags.GetString("mac-address") + if err != nil { + return nil, err + } + if len(m) > 0 { + // if pod create --infra=false + if infra, err := flags.GetBool("infra"); err == nil && !infra { + return nil, errors.Wrap(define.ErrInvalidArg, "cannot set --mac without infra container") + } + mac, err := net.ParseMAC(m) + if err != nil { + return nil, err + } + if !opts.Network.IsBridge() && !opts.Network.IsDefault() { + return nil, errors.Wrap(define.ErrInvalidArg, "--mac-address can only be set when the network mode is bridge") + } + if len(opts.Networks) != 1 { + return nil, errors.Wrap(define.ErrInvalidArg, "--mac-address can only be set for a single network") + } + for name, netOpts := range opts.Networks { + netOpts.StaticMAC = types.HardwareAddr(mac) + opts.Networks[name] = netOpts + } + } + + aliases, err := flags.GetStringSlice("network-alias") + if err != nil { + return nil, err + } + if len(aliases) > 0 { + // if pod create --infra=false + if infra, err := flags.GetBool("infra"); err == nil && !infra { + return nil, errors.Wrap(define.ErrInvalidArg, "cannot set --network-alias without infra container") + } + if !opts.Network.IsBridge() && !opts.Network.IsDefault() { + return nil, errors.Wrap(define.ErrInvalidArg, "--network-alias can only be set when the network mode is bridge") + } + for name, netOpts := range opts.Networks { + netOpts.Aliases = aliases + opts.Networks[name] = netOpts + } + } } return opts, err diff --git a/cmd/podman/containers/checkpoint.go b/cmd/podman/containers/checkpoint.go index e8dd25978..43a1b75e5 100644 --- a/cmd/podman/containers/checkpoint.go +++ b/cmd/podman/containers/checkpoint.go @@ -11,6 +11,7 @@ import ( "github.com/containers/podman/v3/cmd/podman/registry" "github.com/containers/podman/v3/cmd/podman/utils" "github.com/containers/podman/v3/cmd/podman/validate" + "github.com/containers/podman/v3/pkg/criu" "github.com/containers/podman/v3/pkg/domain/entities" "github.com/containers/podman/v3/pkg/rootless" "github.com/containers/storage/pkg/archive" @@ -113,6 +114,9 @@ func checkpoint(cmd *cobra.Command, args []string) error { if checkpointOptions.WithPrevious && checkpointOptions.PreCheckPoint { return errors.Errorf("--with-previous can not be used with --pre-checkpoint") } + if (checkpointOptions.WithPrevious || checkpointOptions.PreCheckPoint) && !criu.MemTrack() { + return errors.New("system (architecture/kernel/CRIU) does not support memory tracking") + } responses, err := registry.ContainerEngine().ContainerCheckpoint(context.Background(), args, checkpointOptions) if err != nil { return err diff --git a/cmd/podman/containers/create.go b/cmd/podman/containers/create.go index a17fcaa1c..9610c29dc 100644 --- a/cmd/podman/containers/create.go +++ b/cmd/podman/containers/create.go @@ -21,6 +21,7 @@ import ( "github.com/containers/podman/v3/pkg/util" "github.com/mattn/go-isatty" "github.com/pkg/errors" + "github.com/sirupsen/logrus" "github.com/spf13/cobra" ) @@ -105,7 +106,7 @@ func create(cmd *cobra.Command, args []string) error { err error ) flags := cmd.Flags() - cliVals.Net, err = common.NetFlagsToNetOptions(nil, *flags, cliVals.Pod == "" && cliVals.PodIDFile == "") + cliVals.Net, err = common.NetFlagsToNetOptions(nil, *flags) if err != nil { return err } @@ -191,6 +192,10 @@ func CreateInit(c *cobra.Command, vals entities.ContainerCreateOptions, isInfra vals.UserNS = "private" } } + if c.Flag("kernel-memory") != nil && c.Flag("kernel-memory").Changed { + logrus.Warnf("The --kernel-memory flag is no longer supported. This flag is a noop.") + } + if cliVals.LogDriver == define.PassthroughLogging { if isatty.IsTerminal(0) || isatty.IsTerminal(1) || isatty.IsTerminal(2) { return vals, errors.New("the '--log-driver passthrough' option cannot be used on a TTY") diff --git a/cmd/podman/containers/kill.go b/cmd/podman/containers/kill.go index 449484449..fe4083df8 100644 --- a/cmd/podman/containers/kill.go +++ b/cmd/podman/containers/kill.go @@ -108,10 +108,13 @@ func kill(_ *cobra.Command, args []string) error { return err } for _, r := range responses { - if r.Err == nil { - fmt.Println(r.RawInput) - } else { + switch { + case r.Err != nil: errs = append(errs, r.Err) + case r.RawInput != "": + fmt.Println(r.RawInput) + default: + fmt.Println(r.Id) } } return errs.PrintErrors() diff --git a/cmd/podman/containers/run.go b/cmd/podman/containers/run.go index 9d1b040cc..b9a2c3bb5 100644 --- a/cmd/podman/containers/run.go +++ b/cmd/podman/containers/run.go @@ -83,6 +83,9 @@ func runFlags(cmd *cobra.Command) { _ = cmd.RegisterFlagCompletionFunc(gpuFlagName, completion.AutocompleteNone) _ = flags.MarkHidden("gpus") + passwdFlagName := "passwd" + flags.BoolVar(&runOpts.Passwd, passwdFlagName, true, "add entries to /etc/passwd and /etc/group") + if registry.IsRemote() { _ = flags.MarkHidden("preserve-fds") _ = flags.MarkHidden("conmon-pidfile") @@ -120,7 +123,7 @@ func run(cmd *cobra.Command, args []string) error { } flags := cmd.Flags() - cliVals.Net, err = common.NetFlagsToNetOptions(nil, *flags, cliVals.Pod == "" && cliVals.PodIDFile == "") + cliVals.Net, err = common.NetFlagsToNetOptions(nil, *flags) if err != nil { return err } @@ -191,6 +194,7 @@ func run(cmd *cobra.Command, args []string) error { return err } s.RawImageName = rawImageName + s.Passwd = &runOpts.Passwd runOpts.Spec = s if _, err := createPodIfNecessary(cmd, s, cliVals.Net); err != nil { diff --git a/cmd/podman/images/build.go b/cmd/podman/images/build.go index 4c563ed27..751db099f 100644 --- a/cmd/podman/images/build.go +++ b/cmd/podman/images/build.go @@ -1,9 +1,11 @@ package images import ( + "fmt" "io" "io/ioutil" "os" + "os/exec" "path/filepath" "strings" "time" @@ -289,7 +291,23 @@ func build(cmd *cobra.Command, args []string) error { } report, err := registry.ImageEngine().Build(registry.GetContext(), containerFiles, *apiBuildOpts) + if err != nil { + exitCode := buildahCLI.ExecErrorCodeGeneric + if registry.IsRemote() { + // errors from server does not contain ExitCode + // so parse exit code from error message + remoteExitCode, parseErr := utils.ExitCodeFromBuildError(fmt.Sprint(errors.Cause(err))) + if parseErr == nil { + exitCode = remoteExitCode + } + } + + if ee, ok := (errors.Cause(err)).(*exec.ExitError); ok { + exitCode = ee.ExitCode() + } + + registry.SetExitCode(exitCode) return err } diff --git a/cmd/podman/images/scp.go b/cmd/podman/images/scp.go index 5c9cadc7a..f02a3c15e 100644 --- a/cmd/podman/images/scp.go +++ b/cmd/podman/images/scp.go @@ -6,18 +6,19 @@ import ( "io/ioutil" urlP "net/url" "os" + "os/exec" + "os/user" "strconv" "strings" "github.com/containers/common/pkg/config" "github.com/containers/podman/v3/cmd/podman/common" - "github.com/containers/podman/v3/cmd/podman/parse" "github.com/containers/podman/v3/cmd/podman/registry" "github.com/containers/podman/v3/cmd/podman/system/connection" "github.com/containers/podman/v3/libpod/define" "github.com/containers/podman/v3/pkg/domain/entities" "github.com/containers/podman/v3/pkg/rootless" - "github.com/docker/distribution/reference" + "github.com/containers/podman/v3/utils" scpD "github.com/dtylman/scp" "github.com/pkg/errors" "github.com/sirupsen/logrus" @@ -28,8 +29,12 @@ import ( var ( saveScpDescription = `Securely copy an image from one host to another.` imageScpCommand = &cobra.Command{ - Use: "scp [options] IMAGE [HOST::]", - Annotations: map[string]string{registry.EngineMode: registry.ABIMode}, + Use: "scp [options] IMAGE [HOST::]", + Annotations: map[string]string{ + registry.UnshareNSRequired: "", + registry.ParentNSRequired: "", + registry.EngineMode: registry.ABIMode, + }, Long: saveScpDescription, Short: "securely copy images", RunE: scp, @@ -40,7 +45,10 @@ var ( ) var ( - scpOpts entities.ImageScpOptions + parentFlags []string + source entities.ImageScpOptions + dest entities.ImageScpOptions + sshInfo entities.ImageScpConnections ) func init() { @@ -53,7 +61,7 @@ func init() { func scpFlags(cmd *cobra.Command) { flags := cmd.Flags() - flags.BoolVarP(&scpOpts.Save.Quiet, "quiet", "q", false, "Suppress the output") + flags.BoolVarP(&source.Quiet, "quiet", "q", false, "Suppress the output") } func scp(cmd *cobra.Command, args []string) (finalErr error) { @@ -61,24 +69,31 @@ func scp(cmd *cobra.Command, args []string) (finalErr error) { // TODO add tag support for images err error ) - if scpOpts.Save.Quiet { // set quiet for both load and save - scpOpts.Load.Quiet = true + for i, val := range os.Args { + if val == "image" { + break + } + if i == 0 { + continue + } + if strings.Contains(val, "CIRRUS") { // need to skip CIRRUS flags for testing suite purposes + continue + } + parentFlags = append(parentFlags, val) } - f, err := ioutil.TempFile("", "podman") // open temp file for load/save output + podman, err := os.Executable() if err != nil { return err } - defer os.Remove(f.Name()) - - scpOpts.Save.Output = f.Name() - scpOpts.Load.Input = scpOpts.Save.Output - if err := parse.ValidateFileName(saveOpts.Output); err != nil { + f, err := ioutil.TempFile("", "podman") // open temp file for load/save output + if err != nil { return err } confR, err := config.NewConfig("") // create a hand made config for the remote engine since we might use remote and native at once if err != nil { return errors.Wrapf(err, "could not make config") } + abiEng, err := registry.NewImageEngine(cmd, args) // abi native engine if err != nil { return err @@ -88,77 +103,115 @@ func scp(cmd *cobra.Command, args []string) (finalErr error) { if err != nil { return err } - serv, err := parseArgs(args, cfg) // parses connection data and "which way" we are loading and saving + locations := []*entities.ImageScpOptions{} + cliConnections := []string{} + flipConnections := false + for _, arg := range args { + loc, connect, err := parseImageSCPArg(arg) + if err != nil { + return err + } + locations = append(locations, loc) + cliConnections = append(cliConnections, connect...) + } + source = *locations[0] + switch { + case len(locations) > 1: + if flipConnections, err = validateSCPArgs(locations); err != nil { + return err + } + if flipConnections { // the order of cliConnections matters, we need to flip both arrays since the args are parsed separately sometimes. + connect := cliConnections[0] + cliConnections[0] = cliConnections[1] + cliConnections[1] = connect + + loc := locations[0] + locations[0] = locations[1] + locations[1] = loc + } + dest = *locations[1] + case len(locations) == 1: + switch { + case len(locations[0].Image) == 0: + return errors.Wrapf(define.ErrInvalidArg, "no source image specified") + case len(locations[0].Image) > 0 && !locations[0].Remote && len(locations[0].User) == 0: // if we have podman image scp $IMAGE + return errors.Wrapf(define.ErrInvalidArg, "must specify a destination") + } + } + + source.File = f.Name() // after parsing the arguments, set the file for the save/load + dest.File = source.File + if err = os.Remove(source.File); err != nil { // remove the file and simply use its name so podman creates the file upon save. avoids umask errors + return err + } + + var serv map[string]config.Destination + serv, err = GetServiceInformation(cliConnections, cfg) if err != nil { return err } + // TODO: Add podman remote support confR.Engine = config.EngineConfig{Remote: true, CgroupManager: "cgroupfs", ServiceDestinations: serv} // pass the service dest (either remote or something else) to engine + saveCmd, loadCmd := createCommands(podman) switch { - case scpOpts.FromRemote: // if we want to load FROM the remote - err = saveToRemote(scpOpts.SourceImageName, scpOpts.Save.Output, "", scpOpts.URI[0], scpOpts.Iden[0]) + case source.Remote: // if we want to load FROM the remote, dest can either be local or remote in this case + err = saveToRemote(source.Image, source.File, "", sshInfo.URI[0], sshInfo.Identities[0]) if err != nil { return err } - if scpOpts.ToRemote { // we want to load remote -> remote - rep, err := loadToRemote(scpOpts.Save.Output, "", scpOpts.URI[1], scpOpts.Iden[1]) + if dest.Remote { // we want to load remote -> remote, both source and dest are remote + rep, err := loadToRemote(dest.File, "", sshInfo.URI[1], sshInfo.Identities[1]) if err != nil { return err } fmt.Println(rep) break } - report, err := abiEng.Load(context.Background(), scpOpts.Load) + err = execPodman(podman, loadCmd) if err != nil { return err } - fmt.Println("Loaded image(s): " + strings.Join(report.Names, ",")) - case scpOpts.ToRemote: // remote host load - scpOpts.Save.Format = "oci-archive" - abiErr := abiEng.Save(context.Background(), scpOpts.SourceImageName, []string{}, scpOpts.Save) // save the image locally before loading it on remote, local, or ssh - if abiErr != nil { - errors.Wrapf(abiErr, "could not save image as specified") - } - rep, err := loadToRemote(scpOpts.Save.Output, "", scpOpts.URI[0], scpOpts.Iden[0]) + case dest.Remote: // remote host load, implies source is local + err = execPodman(podman, saveCmd) if err != nil { return err } - fmt.Println(rep) - // TODO: Add podman remote support - default: // else native load - scpOpts.Save.Format = "oci-archive" - _, err := os.Open(scpOpts.Save.Output) + rep, err := loadToRemote(source.File, "", sshInfo.URI[0], sshInfo.Identities[0]) if err != nil { return err } - if scpOpts.Tag != "" { - return errors.Wrapf(define.ErrInvalidArg, "Renaming of an image is currently not supported") - } - scpOpts.Save.Format = "oci-archive" - abiErr := abiEng.Save(context.Background(), scpOpts.SourceImageName, []string{}, scpOpts.Save) // save the image locally before loading it on remote, local, or ssh - if abiErr != nil { - return errors.Wrapf(abiErr, "could not save image as specified") + fmt.Println(rep) + if err = os.Remove(source.File); err != nil { + return err } - if !rootless.IsRootless() && scpOpts.Rootless { - if scpOpts.User == "" { - scpOpts.User = os.Getenv("SUDO_USER") - if scpOpts.User == "" { - return errors.New("could not obtain root user, make sure the environmental variable SUDO_USER is set, and that this command is being run as root") + // TODO: Add podman remote support + default: // else native load, both source and dest are local and transferring between users + if source.User == "" { // source user has to be set, destination does not + source.User = os.Getenv("USER") + if source.User == "" { + u, err := user.Current() + if err != nil { + return errors.Wrapf(err, "could not obtain user, make sure the environmental variable $USER is set") } + source.User = u.Username } - err := abiEng.Transfer(context.Background(), scpOpts) - if err != nil { - return err - } - } else { - rep, err := abiEng.Load(context.Background(), scpOpts.Load) - if err != nil { - return err - } - fmt.Println("Loaded image(s): " + strings.Join(rep.Names, ",")) + } + err := abiEng.Transfer(context.Background(), source, dest, parentFlags) + if err != nil { + return err } } + src, err := json.MarshalIndent(source, "", " ") + if err != nil { + return err + } + dst, err := json.MarshalIndent(dest, "", " ") + if err != nil { + return err + } + fmt.Printf("SOURCE: %s\nDEST: %s\n", string(src), string(dst)) return nil } @@ -249,119 +302,28 @@ func createConnection(url *urlP.URL, iden string) (*ssh.Client, string, error) { return dialAdd, file, nil } -// validateImageName makes sure that the image given is valid and no injections are occurring -// we simply use this for error checking, bot setting the image -func validateImageName(input string) error { - // ParseNormalizedNamed transforms a shortname image into its - // full name reference so busybox => docker.io/library/busybox - // we want to keep our shortnames, so only return an error if - // we cannot parse what th euser has given us - _, err := reference.ParseNormalizedNamed(input) - return err -} - -// remoteArgLength is a helper function to simplify the extracting of host argument data -// returns an int which contains the length of a specified index in a host::image string -func remoteArgLength(input string, side int) int { - return len((strings.Split(input, "::"))[side]) -} - -// parseArgs returns the valid connection data based off of the information provided by the user -// args is an array of the command arguments and cfg is tooling configuration used to get service destinations -// returned is serv and an error if applicable. serv is a map of service destinations with the connection name as the index -// this connection name is intended to be used as EngineConfig.ServiceDestinations -// this function modifies the global scpOpt entities: FromRemote, ToRemote, Connections, and SourceImageName -func parseArgs(args []string, cfg *config.Config) (map[string]config.Destination, error) { - serv := map[string]config.Destination{} - cliConnections := []string{} - switch len(args) { - case 1: - if strings.Contains(args[0], "localhost") { - if strings.Split(args[0], "@")[0] != "root" { - return nil, errors.Wrapf(define.ErrInvalidArg, "cannot transfer images from any user besides root using sudo") - } - scpOpts.Rootless = true - scpOpts.SourceImageName = strings.Split(args[0], "::")[1] - } else if strings.Contains(args[0], "::") { - scpOpts.FromRemote = true - cliConnections = append(cliConnections, args[0]) - } else { - err := validateImageName(args[0]) - if err != nil { - return nil, err - } - scpOpts.SourceImageName = args[0] - } - case 2: - if strings.Contains(args[0], "localhost") || strings.Contains(args[1], "localhost") { // only supporting root to local using sudo at the moment - if strings.Split(args[0], "@")[0] != "root" { - return nil, errors.Wrapf(define.ErrInvalidArg, "currently, transferring images to a user account is not supported") - } - if len(strings.Split(args[0], "::")) > 1 { - scpOpts.Rootless = true - scpOpts.User = strings.Split(args[1], "@")[0] - scpOpts.SourceImageName = strings.Split(args[0], "::")[1] - } else { - return nil, errors.Wrapf(define.ErrInvalidArg, "currently, you cannot rename images during the transfer or transfer them to a user account") - } - } else if strings.Contains(args[0], "::") { - if !(strings.Contains(args[1], "::")) && remoteArgLength(args[0], 1) == 0 { // if an image is specified, this mean we are loading to our client - cliConnections = append(cliConnections, args[0]) - scpOpts.ToRemote = true - scpOpts.SourceImageName = args[1] - } else if strings.Contains(args[1], "::") { // both remote clients - scpOpts.FromRemote = true - scpOpts.ToRemote = true - if remoteArgLength(args[0], 1) == 0 { // is save->load w/ one image name - cliConnections = append(cliConnections, args[0]) - cliConnections = append(cliConnections, args[1]) - } else if remoteArgLength(args[0], 1) > 0 && remoteArgLength(args[1], 1) > 0 { - //in the future, this function could, instead of rejecting renames, also set a DestImageName field - return nil, errors.Wrapf(define.ErrInvalidArg, "cannot specify an image rename") - } else { // else its a load save (order of args) - cliConnections = append(cliConnections, args[1]) - cliConnections = append(cliConnections, args[0]) - } - } else { - //in the future, this function could, instead of rejecting renames, also set a DestImageName field - return nil, errors.Wrapf(define.ErrInvalidArg, "cannot specify an image rename") - } - } else if strings.Contains(args[1], "::") { // if we are given image host:: - if remoteArgLength(args[1], 1) > 0 { - //in the future, this function could, instead of rejecting renames, also set a DestImageName field - return nil, errors.Wrapf(define.ErrInvalidArg, "cannot specify an image rename") - } - err := validateImageName(args[0]) - if err != nil { - return nil, err - } - scpOpts.SourceImageName = args[0] - scpOpts.ToRemote = true - cliConnections = append(cliConnections, args[1]) - } else { - //in the future, this function could, instead of rejecting renames, also set a DestImageName field - return nil, errors.Wrapf(define.ErrInvalidArg, "cannot specify an image rename") - } - } +// GetSerivceInformation takes the parsed list of hosts to connect to and validates the information +func GetServiceInformation(cliConnections []string, cfg *config.Config) (map[string]config.Destination, error) { + var serv map[string]config.Destination var url string var iden string for i, val := range cliConnections { splitEnv := strings.SplitN(val, "::", 2) - scpOpts.Connections = append(scpOpts.Connections, splitEnv[0]) + sshInfo.Connections = append(sshInfo.Connections, splitEnv[0]) if len(splitEnv[1]) != 0 { err := validateImageName(splitEnv[1]) if err != nil { return nil, err } - scpOpts.SourceImageName = splitEnv[1] + source.Image = splitEnv[1] //TODO: actually use the new name given by the user } - conn, found := cfg.Engine.ServiceDestinations[scpOpts.Connections[i]] + conn, found := cfg.Engine.ServiceDestinations[sshInfo.Connections[i]] if found { url = conn.URI iden = conn.Identity } else { // no match, warn user and do a manual connection. - url = "ssh://" + scpOpts.Connections[i] + url = "ssh://" + sshInfo.Connections[i] iden = "" logrus.Warnf("Unknown connection name given. Please use system connection add to specify the default remote socket location") } @@ -374,8 +336,45 @@ func parseArgs(args []string, cfg *config.Config) (map[string]config.Destination return nil, err } } - scpOpts.URI = append(scpOpts.URI, urlT) - scpOpts.Iden = append(scpOpts.Iden, iden) + sshInfo.URI = append(sshInfo.URI, urlT) + sshInfo.Identities = append(sshInfo.Identities, iden) } return serv, nil } + +// execPodman executes the podman save/load command given the podman binary +func execPodman(podman string, command []string) error { + if rootless.IsRootless() { + cmd := exec.Command(podman) + utils.CreateSCPCommand(cmd, command[1:]) + logrus.Debug("Executing podman command") + return cmd.Run() + } + machinectl, err := exec.LookPath("machinectl") + if err != nil { + cmd := exec.Command("su", "-l", "root", "--command") + cmd = utils.CreateSCPCommand(cmd, []string{strings.Join(command, " ")}) + return cmd.Run() + } + cmd := exec.Command(machinectl, "shell", "-q", "root@.host") + cmd = utils.CreateSCPCommand(cmd, command) + logrus.Debug("Executing load command machinectl") + return cmd.Run() +} + +// createCommands forms the podman save and load commands used by SCP +func createCommands(podman string) ([]string, []string) { + var parentString string + quiet := "" + if source.Quiet { + quiet = "-q " + } + if len(parentFlags) > 0 { + parentString = strings.Join(parentFlags, " ") + " " // if there are parent args, an extra space needs to be added + } else { + parentString = strings.Join(parentFlags, " ") + } + loadCmd := strings.Split(fmt.Sprintf("%s %sload %s--input %s", podman, parentString, quiet, dest.File), " ") + saveCmd := strings.Split(fmt.Sprintf("%s %vsave %s--output %s %s", podman, parentString, quiet, source.File, source.Image), " ") + return saveCmd, loadCmd +} diff --git a/cmd/podman/images/scp_test.go b/cmd/podman/images/scp_test.go new file mode 100644 index 000000000..d4d8f8e58 --- /dev/null +++ b/cmd/podman/images/scp_test.go @@ -0,0 +1,46 @@ +package images + +import ( + "testing" + + "github.com/containers/podman/v3/pkg/domain/entities" + "github.com/stretchr/testify/assert" +) + +func TestParseSCPArgs(t *testing.T) { + args := []string{"alpine", "root@localhost::"} + var source *entities.ImageScpOptions + var dest *entities.ImageScpOptions + var err error + source, _, err = parseImageSCPArg(args[0]) + assert.Nil(t, err) + assert.Equal(t, source.Image, "alpine") + + dest, _, err = parseImageSCPArg(args[1]) + assert.Nil(t, err) + assert.Equal(t, dest.Image, "") + assert.Equal(t, dest.User, "root") + + args = []string{"root@localhost::alpine"} + source, _, err = parseImageSCPArg(args[0]) + assert.Nil(t, err) + assert.Equal(t, source.User, "root") + assert.Equal(t, source.Image, "alpine") + + args = []string{"charliedoern@192.168.68.126::alpine", "foobar@192.168.68.126::"} + source, _, err = parseImageSCPArg(args[0]) + assert.Nil(t, err) + assert.True(t, source.Remote) + assert.Equal(t, source.Image, "alpine") + + dest, _, err = parseImageSCPArg(args[1]) + assert.Nil(t, err) + assert.True(t, dest.Remote) + assert.Equal(t, dest.Image, "") + + args = []string{"charliedoern@192.168.68.126::alpine"} + source, _, err = parseImageSCPArg(args[0]) + assert.Nil(t, err) + assert.True(t, source.Remote) + assert.Equal(t, source.Image, "alpine") +} diff --git a/cmd/podman/images/scp_utils.go b/cmd/podman/images/scp_utils.go new file mode 100644 index 000000000..ebb874c1c --- /dev/null +++ b/cmd/podman/images/scp_utils.go @@ -0,0 +1,87 @@ +package images + +import ( + "strings" + + "github.com/containers/image/v5/docker/reference" + "github.com/containers/podman/v3/libpod/define" + "github.com/containers/podman/v3/pkg/domain/entities" + "github.com/pkg/errors" +) + +// parseImageSCPArg returns the valid connection, and source/destination data based off of the information provided by the user +// arg is a string containing one of the cli arguments returned is a filled out source/destination options structs as well as a connections array and an error if applicable +func parseImageSCPArg(arg string) (*entities.ImageScpOptions, []string, error) { + location := entities.ImageScpOptions{} + var err error + cliConnections := []string{} + + switch { + case strings.Contains(arg, "@localhost"): // image transfer between users + location.User = strings.Split(arg, "@")[0] + location, err = validateImagePortion(location, arg) + if err != nil { + return nil, nil, err + } + case strings.Contains(arg, "::"): + location, err = validateImagePortion(location, arg) + if err != nil { + return nil, nil, err + } + location.Remote = true + cliConnections = append(cliConnections, arg) + default: + location.Image = arg + } + return &location, cliConnections, nil +} + +// validateImagePortion is a helper function to validate the image name in an SCP argument +func validateImagePortion(location entities.ImageScpOptions, arg string) (entities.ImageScpOptions, error) { + if remoteArgLength(arg, 1) > 0 { + err := validateImageName(strings.Split(arg, "::")[1]) + if err != nil { + return location, err + } + location.Image = strings.Split(arg, "::")[1] // this will get checked/set again once we validate connections + } + return location, nil +} + +// validateSCPArgs takes the array of source and destination options and checks for common errors +func validateSCPArgs(locations []*entities.ImageScpOptions) (bool, error) { + if len(locations) > 2 { + return false, errors.Wrapf(define.ErrInvalidArg, "cannot specify more than two arguments") + } + switch { + case len(locations[0].Image) > 0 && len(locations[1].Image) > 0: + return false, errors.Wrapf(define.ErrInvalidArg, "cannot specify an image rename") + case len(locations[0].Image) == 0 && len(locations[1].Image) == 0: + return false, errors.Wrapf(define.ErrInvalidArg, "a source image must be specified") + case len(locations[0].Image) == 0 && len(locations[1].Image) != 0: + if locations[0].Remote && locations[1].Remote { + return true, nil // we need to flip the cliConnections array so the save/load connections are in the right place + } + } + return false, nil +} + +// validateImageName makes sure that the image given is valid and no injections are occurring +// we simply use this for error checking, bot setting the image +func validateImageName(input string) error { + // ParseNormalizedNamed transforms a shortname image into its + // full name reference so busybox => docker.io/library/busybox + // we want to keep our shortnames, so only return an error if + // we cannot parse what the user has given us + _, err := reference.ParseNormalizedNamed(input) + return err +} + +// remoteArgLength is a helper function to simplify the extracting of host argument data +// returns an int which contains the length of a specified index in a host::image string +func remoteArgLength(input string, side int) int { + if strings.Contains(input, "::") { + return len((strings.Split(input, "::"))[side]) + } + return -1 +} diff --git a/cmd/podman/machine/init.go b/cmd/podman/machine/init.go index adde887f7..14e87c201 100644 --- a/cmd/podman/machine/init.go +++ b/cmd/podman/machine/init.go @@ -1,4 +1,4 @@ -// +build amd64,!windows arm64,!windows +// +build amd64 arm64 package machine @@ -8,7 +8,6 @@ import ( "github.com/containers/common/pkg/completion" "github.com/containers/podman/v3/cmd/podman/registry" "github.com/containers/podman/v3/pkg/machine" - "github.com/containers/podman/v3/pkg/machine/qemu" "github.com/pkg/errors" "github.com/spf13/cobra" ) @@ -38,6 +37,7 @@ func init() { }) flags := initCmd.Flags() cfg := registry.PodmanConfig() + initOpts.Username = cfg.Config.Machine.User cpusFlagName := "cpus" flags.Uint64Var( @@ -69,6 +69,20 @@ func init() { "now", false, "Start machine now", ) + timezoneFlagName := "timezone" + defaultTz := cfg.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") ImagePathFlagName := "image-path" flags.StringVar(&initOpts.ImagePath, ImagePathFlagName, cfg.Machine.Image, "Path to qcow image") @@ -82,33 +96,47 @@ func init() { // TODO should we allow for a users to append to the qemu cmdline? func initMachine(cmd *cobra.Command, args []string) error { var ( - vm machine.VM - vmType string - err error + vm machine.VM + err error ) + + provider := getSystemDefaultProvider() initOpts.Name = defaultMachineName if len(args) > 0 { initOpts.Name = args[0] } - switch vmType { - default: // qemu is the default - if _, err := qemu.LoadVMByName(initOpts.Name); err == nil { - return errors.Wrap(machine.ErrVMAlreadyExists, initOpts.Name) - } - vm, err = qemu.NewMachine(initOpts) + if _, err := provider.LoadVMByName(initOpts.Name); err == nil { + return errors.Wrap(machine.ErrVMAlreadyExists, initOpts.Name) } + + vm, err = provider.NewMachine(initOpts) if err != nil { return err } - err = vm.Init(initOpts) - if err != nil { + + 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 } + fmt.Println("Machine init complete") if now { err = vm.Start(initOpts.Name, machine.StartOptions{}) if err == nil { fmt.Printf("Machine %q started successfully\n", initOpts.Name) } + } else { + 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 err } diff --git a/cmd/podman/machine/list.go b/cmd/podman/machine/list.go index 774ab4fd0..858d87401 100644 --- a/cmd/podman/machine/list.go +++ b/cmd/podman/machine/list.go @@ -1,4 +1,5 @@ -// +build amd64,!windows arm64,!windows +//go:build amd64 || arm64 +// +build amd64 arm64 package machine @@ -16,7 +17,6 @@ import ( "github.com/containers/podman/v3/cmd/podman/registry" "github.com/containers/podman/v3/cmd/podman/validate" "github.com/containers/podman/v3/pkg/machine" - "github.com/containers/podman/v3/pkg/machine/qemu" "github.com/docker/go-units" "github.com/pkg/errors" "github.com/spf13/cobra" @@ -69,9 +69,14 @@ func init() { } func list(cmd *cobra.Command, args []string) error { - var opts machine.ListOptions - // We only have qemu VM's for now - listResponse, err := qemu.List(opts) + var ( + opts machine.ListOptions + listResponse []*machine.ListResponse + err error + ) + + provider := getSystemDefaultProvider() + listResponse, err = provider.List(opts) if err != nil { return errors.Wrap(err, "error listing vms") } @@ -182,8 +187,8 @@ func toMachineFormat(vms []*machine.ListResponse) ([]*machineReporter, error) { response.Stream = streamName(vm.Stream) response.VMType = vm.VMType response.CPUs = vm.CPUs - response.Memory = strUint(vm.Memory * units.MiB) - response.DiskSize = strUint(vm.DiskSize * units.GiB) + response.Memory = strUint(vm.Memory) + response.DiskSize = strUint(vm.DiskSize) machineResponses = append(machineResponses, response) } @@ -214,8 +219,8 @@ func toHumanFormat(vms []*machine.ListResponse) ([]*machineReporter, error) { response.Created = units.HumanDuration(time.Since(vm.CreatedAt)) + " ago" response.VMType = vm.VMType response.CPUs = vm.CPUs - response.Memory = units.HumanSize(float64(vm.Memory) * units.MiB) - response.DiskSize = units.HumanSize(float64(vm.DiskSize) * units.GiB) + response.Memory = units.HumanSize(float64(vm.Memory)) + response.DiskSize = units.HumanSize(float64(vm.DiskSize)) humanResponses = append(humanResponses, response) } diff --git a/cmd/podman/machine/machine.go b/cmd/podman/machine/machine.go index 8ff9055f0..22ffbbee7 100644 --- a/cmd/podman/machine/machine.go +++ b/cmd/podman/machine/machine.go @@ -1,4 +1,4 @@ -// +build amd64,!windows arm64,!windows +// +build amd64 arm64 package machine @@ -8,7 +8,6 @@ import ( "github.com/containers/podman/v3/cmd/podman/registry" "github.com/containers/podman/v3/cmd/podman/validate" "github.com/containers/podman/v3/pkg/machine" - "github.com/containers/podman/v3/pkg/machine/qemu" "github.com/spf13/cobra" ) @@ -51,7 +50,8 @@ func autocompleteMachine(cmd *cobra.Command, args []string, toComplete string) ( func getMachines(toComplete string) ([]string, cobra.ShellCompDirective) { suggestions := []string{} - machines, err := qemu.List(machine.ListOptions{}) + provider := getSystemDefaultProvider() + machines, err := provider.List(machine.ListOptions{}) if err != nil { cobra.CompErrorln(err.Error()) return nil, cobra.ShellCompDirectiveNoFileComp diff --git a/cmd/podman/machine/machine_unsupported.go b/cmd/podman/machine/machine_unsupported.go index f8392694a..2f4189446 100644 --- a/cmd/podman/machine/machine_unsupported.go +++ b/cmd/podman/machine/machine_unsupported.go @@ -1,4 +1,4 @@ -// +build !amd64 amd64,windows +// +build !amd64,!arm64 package machine diff --git a/cmd/podman/machine/platform.go b/cmd/podman/machine/platform.go new file mode 100644 index 000000000..fc3186205 --- /dev/null +++ b/cmd/podman/machine/platform.go @@ -0,0 +1,12 @@ +// +build amd64,!windows arm64,!windows + +package machine + +import ( + "github.com/containers/podman/v3/pkg/machine" + "github.com/containers/podman/v3/pkg/machine/qemu" +) + +func getSystemDefaultProvider() machine.Provider { + return qemu.GetQemuProvider() +} diff --git a/cmd/podman/machine/platform_windows.go b/cmd/podman/machine/platform_windows.go new file mode 100644 index 000000000..a4a35e712 --- /dev/null +++ b/cmd/podman/machine/platform_windows.go @@ -0,0 +1,10 @@ +package machine + +import ( + "github.com/containers/podman/v3/pkg/machine" + "github.com/containers/podman/v3/pkg/machine/wsl" +) + +func getSystemDefaultProvider() machine.Provider { + return wsl.GetWSLProvider() +} diff --git a/cmd/podman/machine/rm.go b/cmd/podman/machine/rm.go index c17399c78..c58e74a42 100644 --- a/cmd/podman/machine/rm.go +++ b/cmd/podman/machine/rm.go @@ -1,4 +1,4 @@ -// +build amd64,!windows arm64,!windows +// +build amd64 arm64 package machine @@ -10,7 +10,6 @@ import ( "github.com/containers/podman/v3/cmd/podman/registry" "github.com/containers/podman/v3/pkg/machine" - "github.com/containers/podman/v3/pkg/machine/qemu" "github.com/spf13/cobra" ) @@ -52,18 +51,16 @@ func init() { func rm(cmd *cobra.Command, args []string) error { var ( - err error - vm machine.VM - vmType string + err error + vm machine.VM ) vmName := defaultMachineName if len(args) > 0 && len(args[0]) > 0 { vmName = args[0] } - switch vmType { - default: - vm, err = qemu.LoadVMByName(vmName) - } + + provider := getSystemDefaultProvider() + vm, err = provider.LoadVMByName(vmName) if err != nil { return err } diff --git a/cmd/podman/machine/ssh.go b/cmd/podman/machine/ssh.go index da0a09338..5ef34afc6 100644 --- a/cmd/podman/machine/ssh.go +++ b/cmd/podman/machine/ssh.go @@ -1,4 +1,4 @@ -// +build amd64,!windows arm64,!windows +// +build amd64 arm64 package machine @@ -9,7 +9,6 @@ import ( "github.com/containers/common/pkg/config" "github.com/containers/podman/v3/cmd/podman/registry" "github.com/containers/podman/v3/pkg/machine" - "github.com/containers/podman/v3/pkg/machine/qemu" "github.com/pkg/errors" "github.com/spf13/cobra" ) @@ -47,27 +46,24 @@ func ssh(cmd *cobra.Command, args []string) error { err error validVM bool vm machine.VM - vmType string ) // Set the VM to default vmName := defaultMachineName + provider := getSystemDefaultProvider() // If len is greater than 0, it means we may have been // provided the VM name. If so, we check. The VM name, // if provided, must be in args[0]. if len(args) > 0 { - switch vmType { - default: - validVM, err = qemu.IsValidVMName(args[0]) - if err != nil { - return err - } - if validVM { - vmName = args[0] - } else { - sshOpts.Args = append(sshOpts.Args, args[0]) - } + validVM, err = provider.IsValidVMName(args[0]) + if err != nil { + return err + } + if validVM { + vmName = args[0] + } else { + sshOpts.Args = append(sshOpts.Args, args[0]) } } @@ -88,10 +84,7 @@ func ssh(cmd *cobra.Command, args []string) error { } } - switch vmType { - default: - vm, err = qemu.LoadVMByName(vmName) - } + vm, err = provider.LoadVMByName(vmName) if err != nil { return errors.Wrapf(err, "vm %s not found", vmName) } diff --git a/cmd/podman/machine/start.go b/cmd/podman/machine/start.go index 4ae31e6de..9c9c24f64 100644 --- a/cmd/podman/machine/start.go +++ b/cmd/podman/machine/start.go @@ -1,4 +1,4 @@ -// +build amd64,!windows arm64,!windows +// +build amd64 arm64 package machine @@ -7,7 +7,6 @@ import ( "github.com/containers/podman/v3/cmd/podman/registry" "github.com/containers/podman/v3/pkg/machine" - "github.com/containers/podman/v3/pkg/machine/qemu" "github.com/pkg/errors" "github.com/spf13/cobra" ) @@ -33,30 +32,31 @@ func init() { func start(cmd *cobra.Command, args []string) error { var ( - err error - vm machine.VM - vmType string + err error + vm machine.VM ) vmName := defaultMachineName if len(args) > 0 && len(args[0]) > 0 { vmName = args[0] } - // We only have qemu VM's for now - active, activeName, err := qemu.CheckActiveVM() + provider := getSystemDefaultProvider() + vm, err = provider.LoadVMByName(vmName) if err != nil { return err } + + active, activeName, cerr := provider.CheckExclusiveActiveVM() + if cerr != nil { + return cerr + } if active { if vmName == activeName { return errors.Wrapf(machine.ErrVMAlreadyRunning, "cannot start VM %s", vmName) } return errors.Wrapf(machine.ErrMultipleActiveVM, "cannot start VM %s. VM %s is currently running", vmName, activeName) } - switch vmType { - default: - vm, err = qemu.LoadVMByName(vmName) - } + vm, err = provider.LoadVMByName(vmName) if err != nil { return err } diff --git a/cmd/podman/machine/stop.go b/cmd/podman/machine/stop.go index 75666f734..17969298b 100644 --- a/cmd/podman/machine/stop.go +++ b/cmd/podman/machine/stop.go @@ -1,4 +1,5 @@ -// +build amd64,!windows arm64,!windows +//go:build amd64 || arm64 +// +build amd64 arm64 package machine @@ -7,7 +8,6 @@ import ( "github.com/containers/podman/v3/cmd/podman/registry" "github.com/containers/podman/v3/pkg/machine" - "github.com/containers/podman/v3/pkg/machine/qemu" "github.com/spf13/cobra" ) @@ -33,18 +33,15 @@ func init() { // TODO Name shouldn't be required, need to create a default vm func stop(cmd *cobra.Command, args []string) error { var ( - err error - vm machine.VM - vmType string + err error + vm machine.VM ) vmName := defaultMachineName if len(args) > 0 && len(args[0]) > 0 { vmName = args[0] } - switch vmType { - default: - vm, err = qemu.LoadVMByName(vmName) - } + provider := getSystemDefaultProvider() + vm, err = provider.LoadVMByName(vmName) if err != nil { return err } diff --git a/cmd/podman/main.go b/cmd/podman/main.go index b7f5f1720..b38734617 100644 --- a/cmd/podman/main.go +++ b/cmd/podman/main.go @@ -52,14 +52,14 @@ func parseCommands() *cobra.Command { // Command cannot be run rootless _, found := c.Command.Annotations[registry.UnshareNSRequired] if found { - if rootless.IsRootless() && os.Getuid() != 0 { + if rootless.IsRootless() && os.Getuid() != 0 && c.Command.Name() != "scp" { c.Command.RunE = func(cmd *cobra.Command, args []string) error { return fmt.Errorf("cannot run command %q in rootless mode, must execute `podman unshare` first", cmd.CommandPath()) } } } else { _, found = c.Command.Annotations[registry.ParentNSRequired] - if rootless.IsRootless() && found { + if rootless.IsRootless() && found && c.Command.Name() != "scp" { c.Command.RunE = func(cmd *cobra.Command, args []string) error { return fmt.Errorf("cannot run command %q in rootless mode", cmd.CommandPath()) } diff --git a/cmd/podman/networks/connect.go b/cmd/podman/networks/connect.go index 0d62a45df..b0ffbfe6d 100644 --- a/cmd/podman/networks/connect.go +++ b/cmd/podman/networks/connect.go @@ -1,9 +1,12 @@ package network import ( + "net" + "github.com/containers/common/pkg/completion" "github.com/containers/podman/v3/cmd/podman/common" "github.com/containers/podman/v3/cmd/podman/registry" + "github.com/containers/podman/v3/libpod/network/types" "github.com/containers/podman/v3/pkg/domain/entities" "github.com/spf13/cobra" ) @@ -23,13 +26,28 @@ var ( var ( networkConnectOptions entities.NetworkConnectOptions + ipv4 net.IP + ipv6 net.IP + macAddress string ) func networkConnectFlags(cmd *cobra.Command) { flags := cmd.Flags() aliasFlagName := "alias" - flags.StringSliceVar(&networkConnectOptions.Aliases, aliasFlagName, []string{}, "network scoped alias for container") + flags.StringSliceVar(&networkConnectOptions.Aliases, aliasFlagName, nil, "network scoped alias for container") _ = cmd.RegisterFlagCompletionFunc(aliasFlagName, completion.AutocompleteNone) + + ipAddressFlagName := "ip" + flags.IPVar(&ipv4, ipAddressFlagName, nil, "set a static ipv4 address for this container network") + _ = cmd.RegisterFlagCompletionFunc(ipAddressFlagName, completion.AutocompleteNone) + + ipv6AddressFlagName := "ip6" + flags.IPVar(&ipv6, ipv6AddressFlagName, nil, "set a static ipv6 address for this container network") + _ = cmd.RegisterFlagCompletionFunc(ipv6AddressFlagName, completion.AutocompleteNone) + + macAddressFlagName := "mac-address" + flags.StringVar(&macAddress, macAddressFlagName, "", "set a static mac address for this container network") + _ = cmd.RegisterFlagCompletionFunc(macAddressFlagName, completion.AutocompleteNone) } func init() { @@ -42,5 +60,18 @@ func init() { func networkConnect(cmd *cobra.Command, args []string) error { networkConnectOptions.Container = args[1] + if macAddress != "" { + mac, err := net.ParseMAC(macAddress) + if err != nil { + return err + } + networkConnectOptions.StaticMAC = types.HardwareAddr(mac) + } + for _, ip := range []net.IP{ipv4, ipv6} { + if ip != nil { + networkConnectOptions.StaticIPs = append(networkConnectOptions.StaticIPs, ip) + } + } + return registry.ContainerEngine().NetworkConnect(registry.Context(), args[0], networkConnectOptions) } diff --git a/cmd/podman/networks/list.go b/cmd/podman/networks/list.go index 6f1a7742a..7ce566225 100644 --- a/cmd/podman/networks/list.go +++ b/cmd/podman/networks/list.go @@ -3,6 +3,7 @@ package network import ( "fmt" "os" + "sort" "strings" "github.com/containers/common/pkg/completion" @@ -73,6 +74,11 @@ func networkList(cmd *cobra.Command, args []string) error { return err } + // sort the networks to make sure the order is deterministic + sort.Slice(responses, func(i, j int) bool { + return responses[i].Name < responses[j].Name + }) + switch { // quiet means we only print the network names case networkListOptions.Quiet: diff --git a/cmd/podman/play/kube.go b/cmd/podman/play/kube.go index 11b5d7d34..ae4066a6f 100644 --- a/cmd/podman/play/kube.go +++ b/cmd/podman/play/kube.go @@ -69,7 +69,7 @@ func init() { _ = kubeCmd.RegisterFlagCompletionFunc(staticMACFlagName, completion.AutocompleteNone) networkFlagName := "network" - flags.StringVar(&kubeOptions.Network, networkFlagName, "", "Connect pod to CNI network(s)") + flags.StringArrayVar(&kubeOptions.Networks, networkFlagName, nil, "Connect pod to network(s) or network mode") _ = kubeCmd.RegisterFlagCompletionFunc(networkFlagName, common.AutocompleteNetworkFlag) staticIPFlagName := "ip" diff --git a/cmd/podman/pods/create.go b/cmd/podman/pods/create.go index 7399dd029..f844812c2 100644 --- a/cmd/podman/pods/create.go +++ b/cmd/podman/pods/create.go @@ -116,7 +116,7 @@ func create(cmd *cobra.Command, args []string) error { return fmt.Errorf("cannot specify no-hosts without an infra container") } flags := cmd.Flags() - createOptions.Net, err = common.NetFlagsToNetOptions(nil, *flags, createOptions.Infra) + createOptions.Net, err = common.NetFlagsToNetOptions(nil, *flags) if err != nil { return err } @@ -133,7 +133,7 @@ func create(cmd *cobra.Command, args []string) error { } else { // reassign certain options for lbpod api, these need to be populated in spec flags := cmd.Flags() - infraOptions.Net, err = common.NetFlagsToNetOptions(nil, *flags, createOptions.Infra) + infraOptions.Net, err = common.NetFlagsToNetOptions(nil, *flags) if err != nil { return err } diff --git a/cmd/podman/secrets/list.go b/cmd/podman/secrets/list.go index 255d9ae1a..2074ab973 100644 --- a/cmd/podman/secrets/list.go +++ b/cmd/podman/secrets/list.go @@ -8,6 +8,7 @@ import ( "github.com/containers/common/pkg/completion" "github.com/containers/common/pkg/report" "github.com/containers/podman/v3/cmd/podman/common" + "github.com/containers/podman/v3/cmd/podman/parse" "github.com/containers/podman/v3/cmd/podman/registry" "github.com/containers/podman/v3/cmd/podman/validate" "github.com/containers/podman/v3/pkg/domain/entities" @@ -32,6 +33,7 @@ var ( type listFlagType struct { format string noHeading bool + filter []string } func init() { @@ -44,14 +46,26 @@ func init() { formatFlagName := "format" flags.StringVar(&listFlag.format, formatFlagName, "{{.ID}}\t{{.Name}}\t{{.Driver}}\t{{.CreatedAt}}\t{{.UpdatedAt}}\t\n", "Format volume output using Go template") _ = lsCmd.RegisterFlagCompletionFunc(formatFlagName, common.AutocompleteFormat(entities.SecretInfoReport{})) + filterFlagName := "filter" + flags.StringSliceVarP(&listFlag.filter, filterFlagName, "f", []string{}, "Filter secret output") + _ = lsCmd.RegisterFlagCompletionFunc(filterFlagName, common.AutocompleteSecretFilters) flags.BoolVar(&listFlag.noHeading, "noheading", false, "Do not print headers") } func ls(cmd *cobra.Command, args []string) error { - responses, err := registry.ContainerEngine().SecretList(context.Background(), entities.SecretListRequest{}) + var err error + lsOpts := entities.SecretListRequest{} + + lsOpts.Filters, err = parse.FilterArgumentsIntoFilters(listFlag.filter) + if err != nil { + return err + } + + responses, err := registry.ContainerEngine().SecretList(context.Background(), lsOpts) if err != nil { return err } + listed := make([]*entities.SecretListReport, 0, len(responses)) for _, response := range responses { listed = append(listed, &entities.SecretListReport{ diff --git a/cmd/podman/utils/error.go b/cmd/podman/utils/error.go index 2d58bc70d..aab1da675 100644 --- a/cmd/podman/utils/error.go +++ b/cmd/podman/utils/error.go @@ -1,8 +1,13 @@ package utils import ( + "errors" "fmt" "os" + "strconv" + "strings" + + buildahCLI "github.com/containers/buildah/pkg/cli" ) type OutputErrors []error @@ -17,3 +22,24 @@ func (o OutputErrors) PrintErrors() (lastError error) { } return } + +/* For remote client, server does not returns error with exit code + instead returns a message and we cast it to a new error. + + Following function performs parsing on build error and returns + exit status which was exepected for this current build +*/ +func ExitCodeFromBuildError(errorMsg string) (int, error) { + if strings.Contains(errorMsg, "exit status") { + errorSplit := strings.Split(errorMsg, " ") + if errorSplit[len(errorSplit)-2] == "status" { + tmpSplit := strings.Split(errorSplit[len(errorSplit)-1], "\n") + exitCodeRemote, err := strconv.Atoi(tmpSplit[0]) + if err == nil { + return exitCodeRemote, nil + } + return buildahCLI.ExecErrorCodeGeneric, err + } + } + return buildahCLI.ExecErrorCodeGeneric, errors.New("error message does not contains a valid exit code") +} diff --git a/cmd/podman/volumes/list.go b/cmd/podman/volumes/list.go index c372527de..97fa2c61f 100644 --- a/cmd/podman/volumes/list.go +++ b/cmd/podman/volumes/list.go @@ -4,11 +4,11 @@ import ( "context" "fmt" "os" - "strings" "github.com/containers/common/pkg/completion" "github.com/containers/common/pkg/report" "github.com/containers/podman/v3/cmd/podman/common" + "github.com/containers/podman/v3/cmd/podman/parse" "github.com/containers/podman/v3/cmd/podman/registry" "github.com/containers/podman/v3/cmd/podman/validate" "github.com/containers/podman/v3/libpod/define" @@ -64,19 +64,18 @@ func init() { } func list(cmd *cobra.Command, args []string) error { + var err error if cliOpts.Quiet && cmd.Flag("format").Changed { return errors.New("quiet and format flags cannot be used together") } if len(cliOpts.Filter) > 0 { lsOpts.Filter = make(map[string][]string) } - for _, f := range cliOpts.Filter { - filterSplit := strings.SplitN(f, "=", 2) - if len(filterSplit) < 2 { - return errors.Errorf("filter input must be in the form of filter=value: %s is invalid", f) - } - lsOpts.Filter[filterSplit[0]] = append(lsOpts.Filter[filterSplit[0]], filterSplit[1]) + lsOpts.Filter, err = parse.FilterArgumentsIntoFilters(cliOpts.Filter) + if err != nil { + return err } + responses, err := registry.ContainerEngine().VolumeList(context.Background(), lsOpts) if err != nil { return err diff --git a/cmd/podman/volumes/prune.go b/cmd/podman/volumes/prune.go index 1f3cc6913..43b529768 100644 --- a/cmd/podman/volumes/prune.go +++ b/cmd/podman/volumes/prune.go @@ -58,6 +58,9 @@ func prune(cmd *cobra.Command, args []string) error { return err } pruneOptions.Filters, err = parse.FilterArgumentsIntoFilters(filter) + if err != nil { + return err + } if !force { reader := bufio.NewReader(os.Stdin) fmt.Println("WARNING! This will remove all volumes not used by at least one container. The following volumes will be removed:") diff --git a/cmd/winpath/main.go b/cmd/winpath/main.go new file mode 100644 index 000000000..494d1cf3c --- /dev/null +++ b/cmd/winpath/main.go @@ -0,0 +1,184 @@ +//go:build windows +// +build windows + +package main + +import ( + "errors" + "io/fs" + "os" + "path/filepath" + "strings" + "syscall" + "unsafe" + + "golang.org/x/sys/windows/registry" +) + +type operation int + +const ( + HWND_BROADCAST = 0xFFFF + WM_SETTINGCHANGE = 0x001A + SMTO_ABORTIFHUNG = 0x0002 + ERR_BAD_ARGS = 0x000A + OPERATION_FAILED = 0x06AC + Environment = "Environment" + Add operation = iota + Remove + NotSpecified +) + +func main() { + op := NotSpecified + if len(os.Args) >= 2 { + switch os.Args[1] { + case "add": + op = Add + case "remove": + op = Remove + } + } + + // Stay silent since ran from an installer + if op == NotSpecified { + alert("Usage: " + filepath.Base(os.Args[0]) + " [add|remove]\n\nThis utility adds or removes the podman directory to the Windows Path.") + os.Exit(ERR_BAD_ARGS) + } + + if err := modify(op); err != nil { + os.Exit(OPERATION_FAILED) + } +} + +func modify(op operation) error { + exe, err := os.Executable() + if err != nil { + return err + } + exe, err = filepath.EvalSymlinks(exe) + if err != nil { + return err + } + target := filepath.Dir(exe) + + if op == Remove { + return removePathFromRegistry(target) + } + + return addPathToRegistry(target) +} + +// Appends a directory to the Windows Path stored in the registry +func addPathToRegistry(dir string) error { + k, _, err := registry.CreateKey(registry.CURRENT_USER, Environment, registry.WRITE|registry.READ) + if err != nil { + return err + } + + defer k.Close() + + existing, typ, err := k.GetStringValue("Path") + if err != nil { + return err + } + + // Is this directory already on the windows path? + for _, element := range strings.Split(existing, ";") { + if strings.EqualFold(element, dir) { + // Path already added + return nil + } + } + + // If the existing path is empty we don't want to start with a delimiter + if len(existing) > 0 { + existing += ";" + } + + existing += dir + + // It's important to preserve the registry key type so that it will be interpreted correctly + // EXPAND = evaluate variables in the expression, e.g. %PATH% should be expanded to the system path + // STRING = treat the contents as a string literal + if typ == registry.EXPAND_SZ { + err = k.SetExpandStringValue("Path", existing) + } else { + err = k.SetStringValue("Path", existing) + } + + if err == nil { + broadcastEnvironmentChange() + } + + return err +} + +// Removes all occurences of a directory path from the Windows path stored in the registry +func removePathFromRegistry(path string) error { + k, err := registry.OpenKey(registry.CURRENT_USER, Environment, registry.READ|registry.WRITE) + if err != nil { + if errors.Is(err, fs.ErrNotExist) { + // Nothing to cleanup, the Environment registry key does not exist. + return nil + } + return err + } + + defer k.Close() + + existing, typ, err := k.GetStringValue("Path") + if err != nil { + return err + } + + var elements []string + for _, element := range strings.Split(existing, ";") { + if strings.EqualFold(element, path) { + continue + } + elements = append(elements, element) + } + + newPath := strings.Join(elements, ";") + // Preserve value type (see corresponding comment above) + if typ == registry.EXPAND_SZ { + err = k.SetExpandStringValue("Path", newPath) + } else { + err = k.SetStringValue("Path", newPath) + } + + if err == nil { + broadcastEnvironmentChange() + } + + return err +} + +// Sends a notification message to all top level windows informing them the environmental setings have changed. +// Applications such as the Windows command prompt and powershell will know to stop caching stale values on +// subsequent restarts. Since applications block the sender when receiving a message, we set a 3 second timeout +func broadcastEnvironmentChange() { + env, _ := syscall.UTF16PtrFromString(Environment) + user32 := syscall.NewLazyDLL("user32") + proc := user32.NewProc("SendMessageTimeoutW") + millis := 3000 + _, _, _ = proc.Call(HWND_BROADCAST, WM_SETTINGCHANGE, 0, uintptr(unsafe.Pointer(env)), SMTO_ABORTIFHUNG, uintptr(millis), 0) +} + +// Creates an "error" style pop-up window +func alert(caption string) int { + // Error box style + format := 0x10 + + user32 := syscall.NewLazyDLL("user32.dll") + captionPtr, _ := syscall.UTF16PtrFromString(caption) + titlePtr, _ := syscall.UTF16PtrFromString("winpath") + ret, _, _ := user32.NewProc("MessageBoxW").Call( + uintptr(0), + uintptr(unsafe.Pointer(captionPtr)), + uintptr(unsafe.Pointer(titlePtr)), + uintptr(format)) + + return int(ret) +} |