diff options
44 files changed, 1612 insertions, 582 deletions
@@ -460,15 +460,6 @@ changelog: ## Generate changelog $(shell cat $(TMPFILE) >> changelog.txt) $(shell rm $(TMPFILE)) -completions: binaries - install ${SELINUXOPT} -d -m 755 completions/{bash,zsh,fish} - ./bin/podman completion bash --no-desc -f completions/bash/podman - ./bin/podman-remote completion bash --no-desc -f completions/bash/podman-remote - ./bin/podman completion zsh -f completions/zsh/_podman - ./bin/podman-remote completion zsh -f completions/zsh/_podman-remote - ./bin/podman completion fish -f completions/fish/podman.fish - ./bin/podman-remote completion fish -f completions/fish/podman-remote.fish - .PHONY: install install: .gopathok install.bin install.remote install.man install.cni install.systemd ## Install binaries to system locations @@ -620,14 +611,6 @@ install.libseccomp.sudo: .PHONY: validate.completions validate.completions: SHELL:=/usr/bin/env bash # Set shell to bash for this target validate.completions: - # Check that nobody has manually edited the completion scripts - # If this check fails run make completions to restore the correct scripts - diff completions/bash/podman <(./bin/podman completion --no-desc bash) - diff completions/zsh/_podman <(./bin/podman completion zsh) - diff completions/fish/podman.fish <(./bin/podman completion fish) - diff completions/bash/podman-remote <(./bin/podman-remote completion --no-desc bash) - diff completions/zsh/_podman-remote <(./bin/podman-remote completion zsh) - diff completions/fish/podman-remote.fish <(./bin/podman-remote completion fish) # Check if the files can be loaded by the shell . completions/bash/podman if [ -x /bin/zsh ]; then /bin/zsh completions/zsh/_podman; fi @@ -5,7 +5,7 @@ Podman (the POD MANager) is a tool for managing containers and images, volumes mounted into those containers, and pods made from groups of containers. Podman is based on libpod, a library for container lifecycle management that is also contained in this repository. The libpod library provides APIs for managing containers, pods, container images, and volumes. -* [Latest Version: 2.1.1](https://github.com/containers/podman/releases/latest) +* [Latest Version: 2.2.0](https://github.com/containers/podman/releases/latest) * Latest Remote client for Windows * Latest Remote client for MacOs * Latest Static Remote client for Linux diff --git a/cmd/podman/common/create.go b/cmd/podman/common/create.go index 599b430ea..14086ace4 100644 --- a/cmd/podman/common/create.go +++ b/cmd/podman/common/create.go @@ -513,6 +513,14 @@ func DefineCreateFlags(cmd *cobra.Command, cf *ContainerCLIOpts) { ) _ = cmd.RegisterFlagCompletionFunc(pidsLimitFlagName, completion.AutocompleteNone) + platformFlagName := "platform" + createFlags.StringVar( + &cf.Platform, + platformFlagName, "", + "Specify the platform for selecting the image. (Conflicts with override-arch and override-os)", + ) + _ = cmd.RegisterFlagCompletionFunc(platformFlagName, completion.AutocompleteNone) + podFlagName := "pod" createFlags.StringVar( &cf.Pod, diff --git a/cmd/podman/common/create_opts.go b/cmd/podman/common/create_opts.go index 6dc43dbc6..af53a3b67 100644 --- a/cmd/podman/common/create_opts.go +++ b/cmd/podman/common/create_opts.go @@ -78,6 +78,7 @@ type ContainerCLIOpts struct { OverrideVariant string PID string PIDsLimit *int64 + Platform string Pod string PodIDFile string PreserveFDs uint @@ -211,12 +212,6 @@ func ContainerCreateToContainerCLIOpts(cc handlers.CreateContainerConfig, cgroup mounts = append(mounts, mount) } - // volumes - volumes := make([]string, 0, len(cc.Config.Volumes)) - for v := range cc.Config.Volumes { - volumes = append(volumes, v) - } - // dns dns := make([]net.IP, 0, len(cc.HostConfig.DNS)) for _, d := range cc.HostConfig.DNS { @@ -373,7 +368,6 @@ func ContainerCreateToContainerCLIOpts(cc handlers.CreateContainerConfig, cgroup UserNS: string(cc.HostConfig.UsernsMode), UTS: string(cc.HostConfig.UTSMode), Mount: mounts, - Volume: volumes, VolumesFrom: cc.HostConfig.VolumesFrom, Workdir: cc.Config.WorkingDir, Net: &netInfo, @@ -388,6 +382,10 @@ func ContainerCreateToContainerCLIOpts(cc handlers.CreateContainerConfig, cgroup } } + // volumes + if volumes := cc.HostConfig.Binds; len(volumes) > 0 { + cliOpts.Volume = volumes + } if len(cc.HostConfig.BlkioWeightDevice) > 0 { devices := make([]string, 0, len(cc.HostConfig.BlkioWeightDevice)) for _, d := range cc.HostConfig.BlkioWeightDevice { diff --git a/cmd/podman/completion/completion.go b/cmd/podman/completion/completion.go index ead8d1f05..84942a508 100644 --- a/cmd/podman/completion/completion.go +++ b/cmd/podman/completion/completion.go @@ -67,11 +67,7 @@ func completion(cmd *cobra.Command, args []string) error { var err error switch args[0] { case "bash": - if noDesc { - err = cmd.Root().GenBashCompletion(w) - } else { - err = cmd.Root().GenBashCompletionWithDesc(w) - } + err = cmd.Root().GenBashCompletion(w) case "zsh": if noDesc { err = cmd.Root().GenZshCompletionNoDesc(w) diff --git a/cmd/podman/containers/create.go b/cmd/podman/containers/create.go index db920554e..3d87c71a9 100644 --- a/cmd/podman/containers/create.go +++ b/cmd/podman/containers/create.go @@ -18,6 +18,7 @@ import ( "github.com/containers/podman/v2/pkg/specgen" "github.com/containers/podman/v2/pkg/util" "github.com/pkg/errors" + "github.com/sirupsen/logrus" "github.com/spf13/cobra" ) @@ -236,6 +237,21 @@ func pullImage(imageName string) (string, error) { imageMissing = !br.Value } + if cliVals.Platform != "" { + if cliVals.OverrideArch != "" || cliVals.OverrideOS != "" { + return "", errors.Errorf("--platform option can not be specified with --overide-arch or --override-os") + } + split := strings.SplitN(cliVals.Platform, "/", 2) + cliVals.OverrideOS = split[0] + if len(split) > 1 { + cliVals.OverrideArch = split[1] + } + if pullPolicy != config.PullImageAlways { + logrus.Info("--platform causes the pull policy to be \"always\"") + pullPolicy = config.PullImageAlways + } + } + if imageMissing || pullPolicy == config.PullImageAlways { if pullPolicy == config.PullImageNever { return "", errors.Wrapf(define.ErrNoSuchImage, "unable to find a name and tag match for %s in repotags", imageName) diff --git a/cmd/podman/images/pull.go b/cmd/podman/images/pull.go index a6f41688c..f8e1ee226 100644 --- a/cmd/podman/images/pull.go +++ b/cmd/podman/images/pull.go @@ -3,6 +3,7 @@ package images import ( "fmt" "os" + "strings" "github.com/containers/common/pkg/auth" "github.com/containers/common/pkg/completion" @@ -11,6 +12,7 @@ import ( "github.com/containers/podman/v2/cmd/podman/registry" "github.com/containers/podman/v2/pkg/domain/entities" "github.com/containers/podman/v2/pkg/util" + "github.com/pkg/errors" "github.com/spf13/cobra" ) @@ -94,6 +96,10 @@ func pullFlags(cmd *cobra.Command) { flags.StringVar(&pullOptions.OverrideVariant, overrideVariantFlagName, "", " use VARIANT instead of the running architecture variant for choosing images") _ = cmd.RegisterFlagCompletionFunc(overrideVariantFlagName, completion.AutocompleteNone) + platformFlagName := "platform" + flags.String(platformFlagName, "", "Specify the platform for selecting the image. (Conflicts with override-arch and override-os)") + _ = cmd.RegisterFlagCompletionFunc(platformFlagName, completion.AutocompleteNone) + flags.Bool("disable-content-trust", false, "This is a Docker specific option and is a NOOP") flags.BoolVarP(&pullOptions.Quiet, "quiet", "q", false, "Suppress output information when pulling images") flags.StringVar(&pullOptions.SignaturePolicy, "signature-policy", "", "`Pathname` of signature policy file (not usually used)") @@ -127,6 +133,20 @@ func imagePull(cmd *cobra.Command, args []string) error { return err } } + platform, err := cmd.Flags().GetString("platform") + if err != nil { + return err + } + if platform != "" { + if pullOptions.OverrideArch != "" || pullOptions.OverrideOS != "" { + return errors.Errorf("--platform option can not be specified with --overide-arch or --override-os") + } + split := strings.SplitN(platform, "/", 2) + pullOptions.OverrideOS = split[0] + if len(split) > 1 { + pullOptions.OverrideArch = split[1] + } + } if pullOptions.CredentialsCLI != "" { creds, err := util.ParseRegistryCreds(pullOptions.CredentialsCLI) diff --git a/cmd/podman/networks/create.go b/cmd/podman/networks/create.go index 17de2c95d..938b8da77 100644 --- a/cmd/podman/networks/create.go +++ b/cmd/podman/networks/create.go @@ -6,9 +6,11 @@ import ( "github.com/containers/common/pkg/completion" "github.com/containers/podman/v2/cmd/podman/common" + "github.com/containers/podman/v2/cmd/podman/parse" "github.com/containers/podman/v2/cmd/podman/registry" "github.com/containers/podman/v2/libpod/define" "github.com/containers/podman/v2/pkg/domain/entities" + "github.com/pkg/errors" "github.com/spf13/cobra" ) @@ -27,6 +29,7 @@ var ( var ( networkCreateOptions entities.NetworkCreateOptions + labels []string ) func networkCreateFlags(cmd *cobra.Command) { @@ -50,6 +53,10 @@ func networkCreateFlags(cmd *cobra.Command) { flags.StringVar(&networkCreateOptions.MacVLAN, macvlanFlagName, "", "create a Macvlan connection based on this device") _ = cmd.RegisterFlagCompletionFunc(macvlanFlagName, completion.AutocompleteNone) + labelFlagName := "label" + flags.StringArrayVar(&labels, labelFlagName, nil, "set metadata on a network") + _ = cmd.RegisterFlagCompletionFunc(labelFlagName, completion.AutocompleteNone) + // TODO not supported yet // flags.StringVar(&networkCreateOptions.IPamDriver, "ipam-driver", "", "IP Address Management Driver") @@ -81,6 +88,11 @@ func networkCreate(cmd *cobra.Command, args []string) error { } name = args[0] } + var err error + networkCreateOptions.Labels, err = parse.GetAllLabels([]string{}, labels) + if err != nil { + return errors.Wrap(err, "failed to parse labels") + } response, err := registry.ContainerEngine().NetworkCreate(registry.Context(), name, networkCreateOptions) if err != nil { return err diff --git a/cmd/podman/networks/list.go b/cmd/podman/networks/list.go index dcba3f186..6e6bbb07d 100644 --- a/cmd/podman/networks/list.go +++ b/cmd/podman/networks/list.go @@ -16,6 +16,7 @@ import ( "github.com/containers/podman/v2/cmd/podman/validate" "github.com/containers/podman/v2/libpod/network" "github.com/containers/podman/v2/pkg/domain/entities" + "github.com/pkg/errors" "github.com/spf13/cobra" "github.com/spf13/pflag" ) @@ -35,17 +36,18 @@ var ( var ( networkListOptions entities.NetworkListOptions + filters []string ) func networkListFlags(flags *pflag.FlagSet) { formatFlagName := "format" - flags.StringVarP(&networkListOptions.Format, formatFlagName, "f", "", "Pretty-print networks to JSON or using a Go template") + flags.StringVar(&networkListOptions.Format, formatFlagName, "", "Pretty-print networks to JSON or using a Go template") _ = networklistCommand.RegisterFlagCompletionFunc(formatFlagName, common.AutocompleteJSONFormat) flags.BoolVarP(&networkListOptions.Quiet, "quiet", "q", false, "display only names") filterFlagName := "filter" - flags.StringVarP(&networkListOptions.Filter, filterFlagName, "", "", "Provide filter values (e.g. 'name=podman')") + flags.StringArrayVarP(&filters, filterFlagName, "f", nil, "Provide filter values (e.g. 'name=podman')") _ = networklistCommand.RegisterFlagCompletionFunc(filterFlagName, common.AutocompleteNetworkFilters) } @@ -61,14 +63,14 @@ func init() { } func networkList(cmd *cobra.Command, args []string) error { - // validate the filter pattern. - if len(networkListOptions.Filter) > 0 { - tokens := strings.Split(networkListOptions.Filter, "=") - if len(tokens) != 2 { - return fmt.Errorf("invalid filter syntax : %s", networkListOptions.Filter) + networkListOptions.Filters = make(map[string][]string) + for _, f := range filters { + split := strings.SplitN(f, "=", 2) + if len(split) == 1 { + return errors.Errorf("invalid filter %q", f) } + networkListOptions.Filters[split[0]] = append(networkListOptions.Filters[split[0]], split[1]) } - responses, err := registry.ContainerEngine().NetworkList(registry.Context(), networkListOptions) if err != nil { return err @@ -93,6 +95,7 @@ func networkList(cmd *cobra.Command, args []string) error { "CNIVersion": "version", "Version": "version", "Plugins": "plugins", + "Labels": "labels", }) renderHeaders := true row := "{{.Name}}\t{{.Version}}\t{{.Plugins}}\n" @@ -144,3 +147,11 @@ func (n ListPrintReports) Version() string { func (n ListPrintReports) Plugins() string { return network.GetCNIPlugins(n.NetworkConfigList) } + +func (n ListPrintReports) Labels() string { + list := make([]string, 0, len(n.NetworkListReport.Labels)) + for k, v := range n.NetworkListReport.Labels { + list = append(list, k+"="+v) + } + return strings.Join(list, ",") +} diff --git a/docs/source/markdown/podman-create.1.md b/docs/source/markdown/podman-create.1.md index ab390447e..843e2b22f 100644 --- a/docs/source/markdown/podman-create.1.md +++ b/docs/source/markdown/podman-create.1.md @@ -689,6 +689,11 @@ Default is to create a private PID namespace for the container Tune the container's pids limit. Set `0` to have unlimited pids for the container. (default "4096" on systems that support PIDS cgroups). +#### **--platform**=*OS/ARCH* + +Specify the platform for selecting the image. (Conflicts with override-arch and override-os) +The `--platform` option can be used to override the current architecture and operating system. + #### **--pod**=*name* Run container in an existing pod. If you want Podman to make the pod for you, preference the pod name with `new:`. diff --git a/docs/source/markdown/podman-network-create.1.md b/docs/source/markdown/podman-network-create.1.md index cbf9d26dc..d787809cd 100644 --- a/docs/source/markdown/podman-network-create.1.md +++ b/docs/source/markdown/podman-network-create.1.md @@ -40,6 +40,10 @@ Restrict external access of this network Allocate container IP from a range. The range must be a complete subnet and in CIDR notation. The *ip-range* option must be used with a *subnet* option. +#### **--label** + +Set metadata for a network (e.g., --label mykey=value). + #### **--macvlan** Create a *Macvlan* based connection rather than a classic bridge. You must pass an interface name from the host for the diff --git a/docs/source/markdown/podman-network-ls.1.md b/docs/source/markdown/podman-network-ls.1.md index 34b40b3ae..fcba51190 100644 --- a/docs/source/markdown/podman-network-ls.1.md +++ b/docs/source/markdown/podman-network-ls.1.md @@ -14,13 +14,25 @@ Displays a list of existing podman networks. This command is not available for r The `quiet` option will restrict the output to only the network names. -#### **--format**, **-f** +#### **--format** Pretty-print networks to JSON or using a Go template. -#### **--filter** +#### **--filter**, **-f** -Provide filter values (e.g. 'name=podman'). +Filter output based on conditions given. +Multiple filters can be given with multiple uses of the --filter flag. +Filters with the same key work inclusive with the only exception being +`label` which is exclusive. Filters with different keys always work exclusive. + +Valid filters are listed below: + +| **Filter** | **Description** | +| ---------- | ------------------------------------------------------------------------------------- | +| name | [Name] Network name (accepts regex) | +| label | [Key] or [Key=Value] Label assigned to a network | +| plugin | [Plugin] CNI plugins included in a network (e.g `bridge`,`portmap`,`firewall`,`tuning`,`dnsname`,`macvlan`) | +| driver | [Driver] Only `bridge` is supported | ## EXAMPLE diff --git a/docs/source/markdown/podman-pull.1.md b/docs/source/markdown/podman-pull.1.md index 4495cec3a..44a7e83b6 100644 --- a/docs/source/markdown/podman-pull.1.md +++ b/docs/source/markdown/podman-pull.1.md @@ -106,6 +106,11 @@ Override the OS, defaults to hosts, of the image to be pulled. For example, `win Use _VARIANT_ instead of the default architecture variant of the container image. Some images can use multiple variants of the arm architectures, such as arm/v5 and arm/v7. +#### **--platform**=*OS/ARCH* + +Specify the platform for selecting the image. (Conflicts with override-arch and override-os) +The `--platform` option can be used to override the current architecture and operating system. + #### **--quiet**, **-q** Suppress output information when pulling images diff --git a/docs/source/markdown/podman-run.1.md b/docs/source/markdown/podman-run.1.md index 13b586e17..83aaa33e8 100644 --- a/docs/source/markdown/podman-run.1.md +++ b/docs/source/markdown/podman-run.1.md @@ -717,6 +717,11 @@ The efault is to create a private PID namespace for the container. Tune the container's pids limit. Set to **0** to have unlimited pids for the container. The default is **4096** on systems that support "pids" cgroup controller. +#### **--platform**=*OS/ARCH* + +Specify the platform for selecting the image. (Conflicts with override-arch and override-os) +The `--platform` option can be used to override the current architecture and operating system. + #### **--pod**=*name* Run container in an existing pod. If you want Podman to make the pod for you, prefix the pod name with **new:**. @@ -75,5 +75,3 @@ require ( ) replace github.com/cri-o/ocicni => github.com/cri-o/ocicni v0.2.1-0.20201109200316-afdc16ba66df - -replace github.com/spf13/cobra => github.com/Luap99/cobra v1.0.1-0.20201110155035-83a59186c706 @@ -20,8 +20,6 @@ github.com/Azure/go-autorest v11.1.2+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSW github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= -github.com/Luap99/cobra v1.0.1-0.20201110155035-83a59186c706 h1:KcMtguD/NlxB4c08lzc91o5by51Sf+Ec5+1Yv9Wqvbk= -github.com/Luap99/cobra v1.0.1-0.20201110155035-83a59186c706/go.mod h1:WnodtKOvamDL/PwE2M4iKs8aMDBZ5Q5klgD3qfVJQMI= github.com/Microsoft/go-winio v0.4.11/go.mod h1:VhR8bwka0BXejwEJY73c50VrPtXAaKcyvVC4A4RozmA= github.com/Microsoft/go-winio v0.4.15-0.20190919025122-fc70bd9a86b5/go.mod h1:tTuCMEN+UleMWgg9dVx4Hu52b1bJo+59jBh3ajtinzw= github.com/Microsoft/go-winio v0.4.15-0.20200113171025-3fe6c5262873 h1:93nQ7k53GjoMQ07HVP8g6Zj1fQZDDj7Xy2VkNNtvX8o= @@ -502,6 +500,9 @@ github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4k github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cobra v0.0.2-0.20171109065643-2da4a54c5cee/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= +github.com/spf13/cobra v1.1.1 h1:KfztREH0tPxJJ+geloSLaAkaPkr4ki2Er5quFV1TDo4= +github.com/spf13/cobra v1.1.1/go.mod h1:WnodtKOvamDL/PwE2M4iKs8aMDBZ5Q5klgD3qfVJQMI= github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= github.com/spf13/pflag v0.0.0-20170130214245-9ff6c6923cff/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.1-0.20171106142849-4c012f6dcd95/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= diff --git a/libpod/network/create.go b/libpod/network/create.go index 7e4fc574a..cac438963 100644 --- a/libpod/network/create.go +++ b/libpod/network/create.go @@ -169,7 +169,7 @@ func createBridge(name string, options entities.NetworkCreateOptions, runtimeCon } // create CNI plugin configuration - ncList := NewNcList(name, version.Current()) + ncList := NewNcList(name, version.Current(), options.Labels) var plugins []CNIPlugins // TODO need to iron out the role of isDefaultGW and IPMasq bridge := NewHostLocalBridge(bridgeDeviceName, isGateway, false, ipMasq, ipamConfig) @@ -223,7 +223,7 @@ func createMacVLAN(name string, options entities.NetworkCreateOptions, runtimeCo return "", err } } - ncList := NewNcList(name, version.Current()) + ncList := NewNcList(name, version.Current(), options.Labels) macvlan := NewMacVLANPlugin(options.MacVLAN) plugins = append(plugins, macvlan) ncList["plugins"] = plugins diff --git a/libpod/network/files.go b/libpod/network/files.go index 7f1e3ee18..83cb1c23a 100644 --- a/libpod/network/files.go +++ b/libpod/network/files.go @@ -12,6 +12,7 @@ import ( "github.com/containers/common/pkg/config" "github.com/containers/podman/v2/libpod/define" "github.com/pkg/errors" + "github.com/sirupsen/logrus" ) // ErrNoSuchNetworkInterface indicates that no network interface exists @@ -89,6 +90,35 @@ func GetCNIPlugins(list *libcni.NetworkConfigList) string { return strings.Join(plugins, ",") } +// GetNetworkLabels returns a list of labels as a string +func GetNetworkLabels(list *libcni.NetworkConfigList) NcLabels { + cniJSON := make(map[string]interface{}) + err := json.Unmarshal(list.Bytes, &cniJSON) + if err != nil { + logrus.Errorf("failed to unmarshal network config %v %v", cniJSON["name"], err) + return nil + } + if args, ok := cniJSON["args"]; ok { + if key, ok := args.(map[string]interface{}); ok { + if labels, ok := key[PodmanLabelKey]; ok { + if labels, ok := labels.(map[string]interface{}); ok { + result := make(NcLabels, len(labels)) + for k, v := range labels { + if v, ok := v.(string); ok { + result[k] = v + } else { + logrus.Errorf("network config %v invalid label value type %T should be string", cniJSON["name"], labels) + } + } + return result + } + logrus.Errorf("network config %v invalid label type %T should be map[string]string", cniJSON["name"], labels) + } + } + } + return nil +} + // GetNetworksFromFilesystem gets all the networks from the cni configuration // files func GetNetworksFromFilesystem(config *config.Config) ([]*allocator.Net, error) { diff --git a/libpod/network/netconflist.go b/libpod/network/netconflist.go index ee9adce14..3db38485b 100644 --- a/libpod/network/netconflist.go +++ b/libpod/network/netconflist.go @@ -4,6 +4,11 @@ import ( "net" "os" "path/filepath" + "strings" + + "github.com/containernetworking/cni/libcni" + "github.com/containers/podman/v2/pkg/util" + "github.com/pkg/errors" ) const ( @@ -14,12 +19,24 @@ const ( // NcList describes a generic map type NcList map[string]interface{} +// NcArgs describes the cni args field +type NcArgs map[string]NcLabels + +// NcLabels describes the label map +type NcLabels map[string]string + +// PodmanLabelKey key used to store the podman network label in a cni config +const PodmanLabelKey = "podman_labels" + // NewNcList creates a generic map of values with string // keys and adds in version and network name -func NewNcList(name, version string) NcList { +func NewNcList(name, version string, labels NcLabels) NcList { n := NcList{} n["cniVersion"] = version n["name"] = name + if len(labels) > 0 { + n["args"] = NcArgs{PodmanLabelKey: labels} + } return n } @@ -159,3 +176,64 @@ func NewMacVLANPlugin(device string) MacVLANConfig { } return m } + +// IfPassesFilter filters NetworkListReport and returns true if the filter match the given config +func IfPassesFilter(netconf *libcni.NetworkConfigList, filters map[string][]string) (bool, error) { + result := true + for key, filterValues := range filters { + result = false + switch strings.ToLower(key) { + case "name": + // matches one name, regex allowed + result = util.StringMatchRegexSlice(netconf.Name, filterValues) + + case "plugin": + // match one plugin + plugins := GetCNIPlugins(netconf) + for _, val := range filterValues { + if strings.Contains(plugins, val) { + result = true + break + } + } + + case "label": + // matches all labels + labels := GetNetworkLabels(netconf) + outer: + for _, filterValue := range filterValues { + filterArray := strings.SplitN(filterValue, "=", 2) + filterKey := filterArray[0] + if len(filterArray) > 1 { + filterValue = filterArray[1] + } else { + filterValue = "" + } + for labelKey, labelValue := range labels { + if labelKey == filterKey && ("" == filterValue || labelValue == filterValue) { + result = true + continue outer + } + } + result = false + } + + case "driver": + // matches only for the DefaultNetworkDriver + for _, filterValue := range filterValues { + plugins := GetCNIPlugins(netconf) + if filterValue == DefaultNetworkDriver && + strings.Contains(plugins, DefaultNetworkDriver) { + result = true + } + } + + // TODO: add dangling filter + // TODO TODO: add id filter if we support ids + + default: + return false, errors.Errorf("invalid filter %q", key) + } + } + return result, nil +} diff --git a/libpod/runtime_pod_infra_linux.go b/libpod/runtime_pod_infra_linux.go index 76419587a..3e4185db1 100644 --- a/libpod/runtime_pod_infra_linux.go +++ b/libpod/runtime_pod_infra_linux.go @@ -34,40 +34,56 @@ func (r *Runtime) makeInfraContainer(ctx context.Context, p *Pod, imgName, rawIm // Set Pod hostname g.Config.Hostname = p.config.Hostname + var options []CtrCreateOption + + // Command: If user-specified, use that preferentially. + // If not set and the config file is set, fall back to that. + var infraCtrCommand []string + if p.config.InfraContainer.InfraCommand != nil { + logrus.Debugf("User-specified infra container entrypoint %v", p.config.InfraContainer.InfraCommand) + infraCtrCommand = p.config.InfraContainer.InfraCommand + } else if r.config.Engine.InfraCommand != "" { + logrus.Debugf("Config-specified infra container entrypoint %s", r.config.Engine.InfraCommand) + infraCtrCommand = []string{r.config.Engine.InfraCommand} + } + // Only if set by the user or containers.conf, we set entrypoint for the + // infra container. + // This is only used by commit, so it shouldn't matter... But someone + // may eventually want to commit an infra container? + // TODO: Should we actually do this if set by containers.conf? + if infraCtrCommand != nil { + // Need to duplicate the array - we are going to add Cmd later + // so the current array will be changed. + newArr := make([]string, 0, len(infraCtrCommand)) + newArr = append(newArr, infraCtrCommand...) + options = append(options, WithEntrypoint(newArr)) + } + isRootless := rootless.IsRootless() - entrypointSet := len(p.config.InfraContainer.InfraCommand) > 0 - entryPoint := p.config.InfraContainer.InfraCommand - entryCmd := []string{} - var options []CtrCreateOption // I've seen circumstances where config is being passed as nil. // Let's err on the side of safety and make sure it's safe to use. if config != nil { - // default to entrypoint in image if there is one - if !entrypointSet { - if len(config.Entrypoint) > 0 { - entrypointSet = true - entryPoint = config.Entrypoint - entryCmd = config.Entrypoint + if infraCtrCommand == nil { + // If we have no entrypoint and command from the image, + // we can't go on - the infra container has no command. + if len(config.Entrypoint) == 0 && len(config.Cmd) == 0 { + return nil, errors.Errorf("infra container has no command") } - } else { // so use the InfraCommand - entrypointSet = true - entryCmd = entryPoint - } - - if len(config.Cmd) > 0 { - // We can't use the default pause command, since we're - // sourcing from the image. If we didn't already set an - // entrypoint, set one now. - if !entrypointSet { + if len(config.Entrypoint) > 0 { + infraCtrCommand = config.Entrypoint + } else { // Use the Docker default "/bin/sh -c" // entrypoint, as we're overriding command. // If an image doesn't want this, it can // override entrypoint too. - entryCmd = []string{"/bin/sh", "-c"} + infraCtrCommand = []string{"/bin/sh", "-c"} } - entryCmd = append(entryCmd, config.Cmd...) } + if len(config.Cmd) > 0 { + infraCtrCommand = append(infraCtrCommand, config.Cmd...) + } + if len(config.Env) > 0 { for _, nameValPair := range config.Env { nameValSlice := strings.Split(nameValPair, "=") @@ -127,9 +143,9 @@ func (r *Runtime) makeInfraContainer(ctx context.Context, p *Pod, imgName, rawIm } g.SetRootReadonly(true) - g.SetProcessArgs(entryCmd) + g.SetProcessArgs(infraCtrCommand) - logrus.Debugf("Using %q as infra container entrypoint", entryCmd) + logrus.Debugf("Using %q as infra container command", infraCtrCommand) g.RemoveMount("/dev/shm") if isRootless { @@ -148,9 +164,6 @@ func (r *Runtime) makeInfraContainer(ctx context.Context, p *Pod, imgName, rawIm options = append(options, WithRootFSFromImage(imgID, imgName, rawImageName)) options = append(options, WithName(containerName)) options = append(options, withIsInfra()) - if entrypointSet { - options = append(options, WithEntrypoint(entryPoint)) - } if len(p.config.InfraContainer.ConmonPidFile) > 0 { options = append(options, WithConmonPidFile(p.config.InfraContainer.ConmonPidFile)) } diff --git a/pkg/api/handlers/compat/containers_archive.go b/pkg/api/handlers/compat/containers_archive.go index 293a17e0f..1dd563393 100644 --- a/pkg/api/handlers/compat/containers_archive.go +++ b/pkg/api/handlers/compat/containers_archive.go @@ -1,12 +1,379 @@ package compat import ( - "errors" - "net/http" + "bytes" + "encoding/base64" + "encoding/json" + "fmt" + "path/filepath" + "strings" + "github.com/containers/buildah/copier" + "github.com/containers/buildah/pkg/chrootuser" + "github.com/containers/podman/v2/libpod" + "github.com/containers/podman/v2/libpod/define" "github.com/containers/podman/v2/pkg/api/handlers/utils" + "github.com/containers/storage/pkg/idtools" + "github.com/opencontainers/runtime-spec/specs-go" + + "net/http" + "os" + "time" + + "github.com/gorilla/schema" + "github.com/pkg/errors" + "github.com/sirupsen/logrus" ) func Archive(w http.ResponseWriter, r *http.Request) { - utils.Error(w, "not implemented", http.StatusNotImplemented, errors.New("not implemented")) + decoder := r.Context().Value("decoder").(*schema.Decoder) + runtime := r.Context().Value("runtime").(*libpod.Runtime) + + switch r.Method { + case http.MethodPut: + handlePut(w, r, decoder, runtime) + case http.MethodGet, http.MethodHead: + handleHeadOrGet(w, r, decoder, runtime) + default: + utils.Error(w, fmt.Sprintf("not implemented, method: %v", r.Method), http.StatusNotImplemented, errors.New(fmt.Sprintf("not implemented, method: %v", r.Method))) + } +} + +func handleHeadOrGet(w http.ResponseWriter, r *http.Request, decoder *schema.Decoder, runtime *libpod.Runtime) { + query := struct { + Path string `schema:"path"` + }{} + + err := decoder.Decode(&query, r.URL.Query()) + if err != nil { + utils.Error(w, "Bad Request.", http.StatusBadRequest, errors.Wrap(err, "couldn't decode the query")) + return + } + + if query.Path == "" { + utils.Error(w, "Bad Request.", http.StatusBadRequest, errors.New("missing `path` parameter")) + return + } + + containerName := utils.GetName(r) + + ctr, err := runtime.LookupContainer(containerName) + if errors.Cause(err) == define.ErrNoSuchCtr { + utils.Error(w, "Not found.", http.StatusNotFound, errors.Wrap(err, "the container doesn't exists")) + return + } else if err != nil { + utils.Error(w, "Something went wrong.", http.StatusInternalServerError, err) + return + } + + mountPoint, err := ctr.Mount() + if err != nil { + utils.Error(w, "Something went wrong.", http.StatusInternalServerError, errors.Wrap(err, "failed to mount the container")) + return + } + + defer func() { + if err := ctr.Unmount(false); err != nil { + logrus.Warnf("failed to unmount container %s: %q", containerName, err) + } + }() + + opts := copier.StatOptions{} + + mountPoint, path, err := fixUpMountPointAndPath(runtime, ctr, mountPoint, query.Path) + if err != nil { + utils.Error(w, "Something went wrong.", http.StatusInternalServerError, err) + return + } + + stats, err := copier.Stat(mountPoint, "", opts, []string{filepath.Join(mountPoint, path)}) + if err != nil { + utils.Error(w, "Something went wrong.", http.StatusInternalServerError, errors.Wrap(err, "failed to get stats about file")) + return + } + + if len(stats) <= 0 || len(stats[0].Globbed) <= 0 { + errs := make([]string, 0, len(stats)) + + for _, stat := range stats { + if stat.Error != "" { + errs = append(errs, stat.Error) + } + } + + utils.Error(w, "Not found.", http.StatusNotFound, fmt.Errorf("file doesn't exist (errs: %q)", strings.Join(errs, ";"))) + + return + } + + statHeader, err := statsToHeader(stats[0].Results[stats[0].Globbed[0]]) + if err != nil { + utils.Error(w, "Something went wrong.", http.StatusInternalServerError, err) + return + } + + w.Header().Add("X-Docker-Container-Path-Stat", statHeader) + + if r.Method == http.MethodGet { + idMappingOpts, err := ctr.IDMappings() + if err != nil { + utils.Error(w, "Not found.", http.StatusInternalServerError, + errors.Wrapf(err, "error getting IDMappingOptions")) + return + } + + destOwner := idtools.IDPair{UID: os.Getuid(), GID: os.Getgid()} + + opts := copier.GetOptions{ + UIDMap: idMappingOpts.UIDMap, + GIDMap: idMappingOpts.GIDMap, + ChownDirs: &destOwner, + ChownFiles: &destOwner, + KeepDirectoryNames: true, + } + + w.WriteHeader(http.StatusOK) + + err = copier.Get(mountPoint, "", opts, []string{filepath.Join(mountPoint, path)}, w) + if err != nil { + logrus.Error(errors.Wrapf(err, "failed to copy from the %s container path %s", containerName, query.Path)) + return + } + } else { + w.WriteHeader(http.StatusOK) + } +} + +func handlePut(w http.ResponseWriter, r *http.Request, decoder *schema.Decoder, runtime *libpod.Runtime) { + query := struct { + Path string `schema:"path"` + // TODO handle params below + NoOverwriteDirNonDir bool `schema:"noOverwriteDirNonDir"` + CopyUIDGID bool `schema:"copyUIDGID"` + }{} + + err := decoder.Decode(&query, r.URL.Query()) + if err != nil { + utils.Error(w, "Bad Request.", http.StatusBadRequest, errors.Wrap(err, "couldn't decode the query")) + return + } + + ctrName := utils.GetName(r) + + ctr, err := runtime.LookupContainer(ctrName) + if err != nil { + utils.Error(w, "Not found", http.StatusNotFound, errors.Wrapf(err, "the %s container doesn't exists", ctrName)) + return + } + + mountPoint, err := ctr.Mount() + if err != nil { + utils.Error(w, "Something went wrong", http.StatusInternalServerError, errors.Wrapf(err, "failed to mount the %s container", ctrName)) + return + } + + defer func() { + if err := ctr.Unmount(false); err != nil { + logrus.Warnf("failed to unmount container %s", ctrName) + } + }() + + user, err := getUser(mountPoint, ctr.User()) + if err != nil { + utils.Error(w, "Something went wrong", http.StatusInternalServerError, err) + return + } + + idMappingOpts, err := ctr.IDMappings() + if err != nil { + utils.Error(w, "Something went wrong", http.StatusInternalServerError, errors.Wrapf(err, "error getting IDMappingOptions")) + return + } + + destOwner := idtools.IDPair{UID: int(user.UID), GID: int(user.GID)} + + opts := copier.PutOptions{ + UIDMap: idMappingOpts.UIDMap, + GIDMap: idMappingOpts.GIDMap, + ChownDirs: &destOwner, + ChownFiles: &destOwner, + } + + mountPoint, path, err := fixUpMountPointAndPath(runtime, ctr, mountPoint, query.Path) + if err != nil { + utils.Error(w, "Something went wrong", http.StatusInternalServerError, err) + return + } + + w.WriteHeader(http.StatusOK) + + err = copier.Put(mountPoint, filepath.Join(mountPoint, path), opts, r.Body) + if err != nil { + logrus.Error(errors.Wrapf(err, "failed to copy to the %s container path %s", ctrName, query.Path)) + return + } +} + +func statsToHeader(stats *copier.StatForItem) (string, error) { + statsDTO := struct { + Name string `json:"name"` + Size int64 `json:"size"` + Mode os.FileMode `json:"mode"` + ModTime time.Time `json:"mtime"` + LinkTarget string `json:"linkTarget"` + }{ + Name: filepath.Base(stats.Name), + Size: stats.Size, + Mode: stats.Mode, + ModTime: stats.ModTime, + LinkTarget: stats.ImmediateTarget, + } + + jsonBytes, err := json.Marshal(&statsDTO) + if err != nil { + return "", errors.Wrap(err, "failed to serialize file stats") + } + + buff := bytes.NewBuffer(make([]byte, 0, 128)) + base64encoder := base64.NewEncoder(base64.StdEncoding, buff) + + _, err = base64encoder.Write(jsonBytes) + if err != nil { + return "", err + } + + err = base64encoder.Close() + if err != nil { + return "", err + } + + return buff.String(), nil +} + +// the utility functions below are copied from abi/cp.go + +func getUser(mountPoint string, userspec string) (specs.User, error) { + uid, gid, _, err := chrootuser.GetUser(mountPoint, userspec) + u := specs.User{ + UID: uid, + GID: gid, + Username: userspec, + } + + if !strings.Contains(userspec, ":") { + groups, err2 := chrootuser.GetAdditionalGroupsForUser(mountPoint, uint64(u.UID)) + if err2 != nil { + if errors.Cause(err2) != chrootuser.ErrNoSuchUser && err == nil { + err = err2 + } + } else { + u.AdditionalGids = groups + } + } + + return u, err +} + +func fixUpMountPointAndPath(runtime *libpod.Runtime, ctr *libpod.Container, mountPoint, ctrPath string) (string, string, error) { + if !filepath.IsAbs(ctrPath) { + endsWithSep := strings.HasSuffix(ctrPath, string(filepath.Separator)) + ctrPath = filepath.Join(ctr.WorkingDir(), ctrPath) + + if endsWithSep { + ctrPath = ctrPath + string(filepath.Separator) + } + } + if isVol, volDestName, volName := isVolumeDestName(ctrPath, ctr); isVol { //nolint(gocritic) + newMountPoint, path, err := pathWithVolumeMount(runtime, volDestName, volName, ctrPath) + if err != nil { + return "", "", errors.Wrapf(err, "error getting source path from volume %s", volDestName) + } + + mountPoint = newMountPoint + ctrPath = path + } else if isBindMount, mount := isBindMountDestName(ctrPath, ctr); isBindMount { //nolint(gocritic) + newMountPoint, path := pathWithBindMountSource(mount, ctrPath) + mountPoint = newMountPoint + ctrPath = path + } + + return mountPoint, ctrPath, nil +} + +func isVolumeDestName(path string, ctr *libpod.Container) (bool, string, string) { + separator := string(os.PathSeparator) + + if filepath.IsAbs(path) { + path = strings.TrimPrefix(path, separator) + } + + if path == "" { + return false, "", "" + } + + for _, vol := range ctr.Config().NamedVolumes { + volNamePath := strings.TrimPrefix(vol.Dest, separator) + if matchVolumePath(path, volNamePath) { + return true, vol.Dest, vol.Name + } + } + + return false, "", "" +} + +func pathWithVolumeMount(runtime *libpod.Runtime, volDestName, volName, path string) (string, string, error) { + destVolume, err := runtime.GetVolume(volName) + if err != nil { + return "", "", errors.Wrapf(err, "error getting volume destination %s", volName) + } + + if !filepath.IsAbs(path) { + path = filepath.Join(string(os.PathSeparator), path) + } + + return destVolume.MountPoint(), strings.TrimPrefix(path, volDestName), err +} + +func isBindMountDestName(path string, ctr *libpod.Container) (bool, specs.Mount) { + separator := string(os.PathSeparator) + + if filepath.IsAbs(path) { + path = strings.TrimPrefix(path, string(os.PathSeparator)) + } + + if path == "" { + return false, specs.Mount{} + } + + for _, m := range ctr.Config().Spec.Mounts { + if m.Type != "bind" { + continue + } + + mDest := strings.TrimPrefix(m.Destination, separator) + if matchVolumePath(path, mDest) { + return true, m + } + } + + return false, specs.Mount{} +} + +func matchVolumePath(path, target string) bool { + pathStr := filepath.Clean(path) + target = filepath.Clean(target) + + for len(pathStr) > len(target) && strings.Contains(pathStr, string(os.PathSeparator)) { + pathStr = pathStr[:strings.LastIndex(pathStr, string(os.PathSeparator))] + } + + return pathStr == target +} + +func pathWithBindMountSource(m specs.Mount, path string) (string, string) { + if !filepath.IsAbs(path) { + path = filepath.Join(string(os.PathSeparator), path) + } + + return m.Source, strings.TrimPrefix(path, m.Destination) } diff --git a/pkg/api/handlers/compat/networks.go b/pkg/api/handlers/compat/networks.go index c74cdb840..762f88a68 100644 --- a/pkg/api/handlers/compat/networks.go +++ b/pkg/api/handlers/compat/networks.go @@ -50,7 +50,7 @@ func InspectNetwork(w http.ResponseWriter, r *http.Request) { utils.NetworkNotFound(w, name, err) return } - report, err := getNetworkResourceByName(name, runtime) + report, err := getNetworkResourceByName(name, runtime, nil) if err != nil { utils.InternalServerError(w, err) return @@ -58,7 +58,7 @@ func InspectNetwork(w http.ResponseWriter, r *http.Request) { utils.WriteResponse(w, http.StatusOK, report) } -func getNetworkResourceByName(name string, runtime *libpod.Runtime) (*types.NetworkResource, error) { +func getNetworkResourceByName(name string, runtime *libpod.Runtime, filters map[string][]string) (*types.NetworkResource, error) { var ( ipamConfigs []dockerNetwork.IPAMConfig ) @@ -85,6 +85,16 @@ func getNetworkResourceByName(name string, runtime *libpod.Runtime) (*types.Netw if err != nil { return nil, err } + if len(filters) > 0 { + ok, err := network.IfPassesFilter(conf, filters) + if err != nil { + return nil, err + } + if !ok { + // do not return the config if we did not match the filter + return nil, nil + } + } // No Bridge plugin means we bail bridge, err := genericPluginsToBridge(conf.Plugins, network.DefaultNetworkDriver) @@ -129,14 +139,14 @@ func getNetworkResourceByName(name string, runtime *libpod.Runtime) (*types.Netw Options: nil, Config: ipamConfigs, }, - Internal: false, + Internal: !bridge.IsGW, Attachable: false, Ingress: false, ConfigFrom: dockerNetwork.ConfigReference{}, ConfigOnly: false, Containers: containerEndpoints, Options: nil, - Labels: nil, + Labels: network.GetNetworkLabels(conf), Peers: nil, Services: nil, } @@ -180,41 +190,23 @@ func ListNetworks(w http.ResponseWriter, r *http.Request) { return } - filterNames, nameFilterExists := query.Filters["name"] - // TODO remove when filters are implemented - if (!nameFilterExists && len(query.Filters) > 0) || len(query.Filters) > 1 { - utils.InternalServerError(w, errors.New("only the name filter for listing networks is implemented")) - return - } netNames, err := network.GetNetworkNamesFromFileSystem(config) if err != nil { utils.InternalServerError(w, err) return } - // filter by name - if nameFilterExists { - names := []string{} - for _, name := range netNames { - for _, filter := range filterNames { - if strings.Contains(name, filter) { - names = append(names, name) - break - } - } - } - netNames = names - } - - reports := make([]*types.NetworkResource, 0, len(netNames)) + var reports []*types.NetworkResource logrus.Errorf("netNames: %q", strings.Join(netNames, ", ")) for _, name := range netNames { - report, err := getNetworkResourceByName(name, runtime) + report, err := getNetworkResourceByName(name, runtime, query.Filters) if err != nil { utils.InternalServerError(w, err) return } - reports = append(reports, report) + if report != nil { + reports = append(reports, report) + } } utils.WriteResponse(w, http.StatusOK, reports) } @@ -245,6 +237,7 @@ func CreateNetwork(w http.ResponseWriter, r *http.Request) { ncOptions := entities.NetworkCreateOptions{ Driver: network.DefaultNetworkDriver, Internal: networkCreate.Internal, + Labels: networkCreate.Labels, } if networkCreate.IPAM != nil && networkCreate.IPAM.Config != nil { if len(networkCreate.IPAM.Config) > 1 { diff --git a/pkg/api/handlers/libpod/networks.go b/pkg/api/handlers/libpod/networks.go index f1578f829..8511e2733 100644 --- a/pkg/api/handlers/libpod/networks.go +++ b/pkg/api/handlers/libpod/networks.go @@ -48,7 +48,7 @@ func ListNetworks(w http.ResponseWriter, r *http.Request) { runtime := r.Context().Value("runtime").(*libpod.Runtime) decoder := r.Context().Value("decoder").(*schema.Decoder) query := struct { - Filter string `schema:"filter"` + Filters map[string][]string `schema:"filters"` }{ // override any golang type defaults } @@ -59,7 +59,7 @@ func ListNetworks(w http.ResponseWriter, r *http.Request) { } options := entities.NetworkListOptions{ - Filter: query.Filter, + Filters: query.Filters, } ic := abi.ContainerEngine{Libpod: runtime} reports, err := ic.NetworkList(r.Context(), options) diff --git a/pkg/api/server/register_networks.go b/pkg/api/server/register_networks.go index ea169cbdf..193b05e6d 100644 --- a/pkg/api/server/register_networks.go +++ b/pkg/api/server/register_networks.go @@ -65,7 +65,11 @@ func (s *APIServer) registerNetworkHandlers(r *mux.Router) error { // - in: query // name: filters // type: string - // description: JSON encoded value of the filters (a map[string][]string) to process on the networks list. Only the name filter is supported. + // description: | + // JSON encoded value of the filters (a map[string][]string) to process on the network list. Currently available filters: + // - name=[name] Matches network name (accepts regex). + // - driver=[driver] Only bridge is supported. + // - label=[key] or label=[key=value] Matches networks based on the presence of a label alone or a label and a value. // produces: // - application/json // responses: @@ -216,9 +220,14 @@ func (s *APIServer) registerNetworkHandlers(r *mux.Router) error { // description: Display summary of network configurations // parameters: // - in: query - // name: filter + // name: filters // type: string - // description: Provide filter values (e.g. 'name=podman') + // description: | + // JSON encoded value of the filters (a map[string][]string) to process on the network list. Available filters: + // - name=[name] Matches network name (accepts regex). + // - driver=[driver] Only bridge is supported. + // - label=[key] or label=[key=value] Matches networks based on the presence of a label alone or a label and a value. + // - plugin=[plugin] Matches CNI plugins included in a network (e.g `bridge`,`portmap`,`firewall`,`tuning`,`dnsname`,`macvlan`) // produces: // - application/json // responses: diff --git a/pkg/bindings/network/network.go b/pkg/bindings/network/network.go index 1d4be8a4c..347f97703 100644 --- a/pkg/bindings/network/network.go +++ b/pkg/bindings/network/network.go @@ -2,6 +2,7 @@ package network import ( "context" + "encoding/json" "net/http" "net/url" "strconv" @@ -79,8 +80,12 @@ func List(ctx context.Context, options entities.NetworkListOptions) ([]*entities return nil, err } params := url.Values{} - if options.Filter != "" { - params.Set("filter", options.Filter) + if options.Filters != nil { + b, err := json.Marshal(options.Filters) + if err != nil { + return nil, err + } + params.Set("filters", string(b)) } response, err := conn.DoRequest(nil, http.MethodGet, "/networks/json", params, nil) if err != nil { diff --git a/pkg/bindings/test/containers_test.go b/pkg/bindings/test/containers_test.go index 0fb677768..15066ff1a 100644 --- a/pkg/bindings/test/containers_test.go +++ b/pkg/bindings/test/containers_test.go @@ -290,17 +290,17 @@ var _ = Describe("Podman containers ", func() { Expect(wait).To(BeNil()) Expect(exitCode).To(BeNumerically("==", -1)) - errChan = make(chan error) + unpauseErrChan := make(chan error) go func() { defer GinkgoRecover() _, waitErr := containers.Wait(bt.conn, name, &running) - errChan <- waitErr - close(errChan) + unpauseErrChan <- waitErr + close(unpauseErrChan) }() err = containers.Unpause(bt.conn, name) Expect(err).To(BeNil()) - unPausewait := <-errChan + unPausewait := <-unpauseErrChan Expect(unPausewait).To(BeNil()) Expect(exitCode).To(BeNumerically("==", -1)) }) diff --git a/pkg/domain/entities/network.go b/pkg/domain/entities/network.go index 86c2e1bcd..f14cac7ef 100644 --- a/pkg/domain/entities/network.go +++ b/pkg/domain/entities/network.go @@ -8,14 +8,15 @@ import ( // NetworkListOptions describes options for listing networks in cli type NetworkListOptions struct { - Format string - Quiet bool - Filter string + Format string + Quiet bool + Filters map[string][]string } // NetworkListReport describes the results from listing networks type NetworkListReport struct { *libcni.NetworkConfigList + Labels map[string]string } // NetworkInspectReport describes the results from inspect networks @@ -39,6 +40,7 @@ type NetworkCreateOptions struct { Driver string Gateway net.IP Internal bool + Labels map[string]string MacVLAN string Range net.IPNet Subnet net.IPNet diff --git a/pkg/domain/infra/abi/network.go b/pkg/domain/infra/abi/network.go index c52584565..6a219edd5 100644 --- a/pkg/domain/infra/abi/network.go +++ b/pkg/domain/infra/abi/network.go @@ -2,10 +2,7 @@ package abi import ( "context" - "fmt" - "strings" - "github.com/containernetworking/cni/libcni" "github.com/containers/podman/v2/libpod/define" "github.com/containers/podman/v2/libpod/network" "github.com/containers/podman/v2/pkg/domain/entities" @@ -26,18 +23,16 @@ func (ic *ContainerEngine) NetworkList(ctx context.Context, options entities.Net return nil, err } - var tokens []string - // tokenize the networkListOptions.Filter in key=value. - if len(options.Filter) > 0 { - tokens = strings.Split(options.Filter, "=") - if len(tokens) != 2 { - return nil, fmt.Errorf("invalid filter syntax : %s", options.Filter) - } - } - for _, n := range networks { - if ifPassesFilterTest(n, tokens) { - reports = append(reports, &entities.NetworkListReport{NetworkConfigList: n}) + ok, err := network.IfPassesFilter(n, options.Filters) + if err != nil { + return nil, err + } + if ok { + reports = append(reports, &entities.NetworkListReport{ + NetworkConfigList: n, + Labels: network.GetNetworkLabels(n), + }) } } return reports, nil @@ -117,28 +112,6 @@ func (ic *ContainerEngine) NetworkCreate(ctx context.Context, name string, optio return network.Create(name, options, runtimeConfig) } -func ifPassesFilterTest(netconf *libcni.NetworkConfigList, filter []string) bool { - result := false - if len(filter) == 0 { - // No filter, so pass - return true - } - switch strings.ToLower(filter[0]) { - case "name": - if filter[1] == netconf.Name { - result = true - } - case "plugin": - plugins := network.GetCNIPlugins(netconf) - if strings.Contains(plugins, filter[1]) { - result = true - } - default: - result = false - } - return result -} - // NetworkDisconnect removes a container from a given network func (ic *ContainerEngine) NetworkDisconnect(ctx context.Context, networkname string, options entities.NetworkDisconnectOptions) error { return ic.Libpod.DisconnectContainerFromNetwork(options.Container, networkname, options.Force) diff --git a/test/apiv2/23-containersArchive.at b/test/apiv2/23-containersArchive.at new file mode 100644 index 000000000..459800196 --- /dev/null +++ b/test/apiv2/23-containersArchive.at @@ -0,0 +1,81 @@ +# -*- sh -*- +# +# test more container-related endpoints +# + +red='\e[31m' +nc='\e[0m' + +podman pull $IMAGE &>/dev/null + +# Ensure clean slate +podman rm -a -f &>/dev/null + +CTR="ArchiveTestingCtr" + +TMPD=$(mktemp -d) +pushd "${TMPD}" +echo "Hello" > "hello.txt" +tar --format=posix -cvf "hello.tar" "hello.txt" &> /dev/null +popd + +HELLO_TAR="${TMPD}/hello.tar" + +podman run -d --name "${CTR}" "${IMAGE}" top + +function cleanUpArchiveTest() { + podman container stop "${CTR}" &> /dev/null + podman container rm "${CTR}" &> /dev/null + rm -fr "${TMPD}" &> /dev/null +} + +t HEAD "containers/nonExistentCtr/archive?path=%2F" 404 +t HEAD "containers/${CTR}/archive?path=%2Fnon%2Fexistent%2Fpath" 404 +t HEAD "containers/${CTR}/archive?path=%2Fetc%2Fpasswd" 200 + +curl "http://$HOST:$PORT/containers/${CTR}/archive?path=%2Ftmp%2F" \ + -X PUT \ + -H "Content-Type: application/x-tar" \ + --upload-file "${HELLO_TAR}" &> /dev/null + +if ! podman exec -it "${CTR}" "grep" "Hello" "/tmp/hello.txt" &> /dev/null ; then + echo -e "${red}NOK: The hello.txt file has not been uploaded.${nc}" 1>&2; + cleanUpArchiveTest + exit 1 +fi + +t HEAD "containers/${CTR}/archive?path=%2Ftmp%2Fhello.txt" 200 + +curl "http://$HOST:$PORT/containers/${CTR}/archive?path=%2Ftmp%2Fhello.txt" \ + --dump-header "${TMPD}/headers.txt" \ + -o "${TMPD}/body.tar" \ + -X GET &> /dev/null + +PATH_STAT="$(grep X-Docker-Container-Path-Stat "${TMPD}/headers.txt" | cut -d " " -f 2 | base64 -d --ignore-garbage)" + +ARCHIVE_TEST_ERROR="" + +if [ "$(echo "${PATH_STAT}" | jq ".name")" != '"hello.txt"' ]; then + echo -e "${red}NOK: Wrong name in X-Docker-Container-Path-Stat header.${nc}" 1>&2; + ARCHIVE_TEST_ERROR="1" +fi + +if [ "$(echo "${PATH_STAT}" | jq ".size")" != "6" ]; then + echo -e "${red}NOK: Wrong size in X-Docker-Container-Path-Stat header.${nc}" 1>&2; + ARCHIVE_TEST_ERROR="1" +fi + +if ! tar -tf "${TMPD}/body.tar" | grep "hello.txt" &> /dev/null; then + echo -e "${red}NOK: Body doesn't contain expected file.${nc}" 1>&2; + ARCHIVE_TEST_ERROR="1" +fi + +if [ "$(tar -xf "${TMPD}/body.tar" hello.txt --to-stdout)" != "Hello" ]; then + echo -e "${red}NOK: Content of file doesn't match.${nc}" 1>&2; + ARCHIVE_TEST_ERROR="1" +fi + +cleanUpArchiveTest +if [[ "${ARCHIVE_TEST_ERROR}" ]] ; then + exit 1; +fi diff --git a/test/apiv2/35-networks.at b/test/apiv2/35-networks.at index ad34511c7..d9556d59f 100644 --- a/test/apiv2/35-networks.at +++ b/test/apiv2/35-networks.at @@ -9,8 +9,8 @@ t GET networks/non-existing-network 404 \ t POST libpod/networks/create?name=network1 '' 200 \ .Filename~.*/network1\\.conflist -# --data '{"Subnet":{"IP":"10.10.254.0","Mask":[255,255,255,0]}}' -t POST libpod/networks/create?name=network2 '"Subnet":{"IP":"10.10.254.0","Mask":[255,255,255,0]}' 200 \ +# --data '{"Subnet":{"IP":"10.10.254.0","Mask":[255,255,255,0]},"Labels":{"abc":"val"}}' +t POST libpod/networks/create?name=network2 '"Subnet":{"IP":"10.10.254.0","Mask":[255,255,255,0]},"Labels":{"abc":"val"}' 200 \ .Filename~.*/network2\\.conflist # test for empty mask @@ -22,7 +22,8 @@ t POST libpod/networks/create '"Subnet":{"IP":"10.10.1.0","Mask":[0,255,255,0]}' # network list t GET libpod/networks/json 200 -t GET libpod/networks/json?filter=name=network1 200 \ +# filters={"name":["network1"]} +t GET libpod/networks/json?filters=%7B%22name%22%3A%5B%22network1%22%5D%7D 200 \ length=1 \ .[0].Name=network1 t GET networks 200 @@ -34,12 +35,12 @@ length=2 #filters={"name":["network"]} t GET networks?filters=%7B%22name%22%3A%5B%22network%22%5D%7D 200 \ length=2 -# invalid filter filters={"label":"abc"} -t GET networks?filters=%7B%22label%22%3A%5B%22abc%22%5D%7D 500 \ -.cause="only the name filter for listing networks is implemented" -# invalid filter filters={"label":"abc","name":["network"]} -t GET networks?filters=%7B%22label%22%3A%22abc%22%2C%22name%22%3A%5B%22network%22%5D%7D 500 \ -.cause="only the name filter for listing networks is implemented" +# filters={"label":["abc"]} +t GET networks?filters=%7B%22label%22%3A%5B%22abc%22%5D%7D 200 \ +length=1 +# invalid filter filters={"id":["abc"]} +t GET networks?filters=%7B%22id%22%3A%5B%22abc%22%5D%7D 500 \ +.cause='invalid filter "id"' # clean the network t DELETE libpod/networks/network1 200 \ diff --git a/test/e2e/create_test.go b/test/e2e/create_test.go index 760345a67..aaf089d97 100644 --- a/test/e2e/create_test.go +++ b/test/e2e/create_test.go @@ -5,6 +5,7 @@ import ( "io/ioutil" "os" "path/filepath" + "runtime" "strings" . "github.com/containers/podman/v2/test/utils" @@ -644,4 +645,35 @@ var _ = Describe("Podman create", func() { Expect(session.ErrorToString()).To(ContainSubstring("unknown flag")) }) + It("podman create --platform", func() { + session := podmanTest.Podman([]string{"create", "--platform=linux/bogus", ALPINE}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Equal(125)) + expectedError := "no image found in manifest list for architecture bogus" + Expect(session.ErrorToString()).To(ContainSubstring(expectedError)) + + session = podmanTest.Podman([]string{"create", "--platform=linux/arm64", "--override-os", "windows", ALPINE}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Equal(125)) + expectedError = "--platform option can not be specified with --overide-arch or --override-os" + Expect(session.ErrorToString()).To(ContainSubstring(expectedError)) + + session = podmanTest.Podman([]string{"create", "-q", "--platform=linux/arm64", ALPINE}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Equal(0)) + + setup := podmanTest.Podman([]string{"container", "inspect", session.OutputToString()}) + setup.WaitWithDefaultTimeout() + Expect(setup.ExitCode()).To(Equal(0)) + + data := setup.InspectContainerToJSON() + setup = podmanTest.Podman([]string{"image", "inspect", data[0].Image}) + setup.WaitWithDefaultTimeout() + Expect(setup.ExitCode()).To(Equal(0)) + + idata := setup.InspectImageJSON() // returns []inspect.ImageData + Expect(len(idata)).To(Equal(1)) + Expect(idata[0].Os).To(Equal(runtime.GOOS)) + Expect(idata[0].Architecture).To(Equal("arm64")) + }) }) diff --git a/test/e2e/network_test.go b/test/e2e/network_test.go index 20e1d5b6b..ad6af61c7 100644 --- a/test/e2e/network_test.go +++ b/test/e2e/network_test.go @@ -66,6 +66,65 @@ var _ = Describe("Podman network", func() { Expect(session.LineInOutputContains(name)).To(BeTrue()) }) + It("podman network list --filter plugin and name", func() { + name, path := generateNetworkConfig(podmanTest) + defer removeConf(path) + + session := podmanTest.Podman([]string{"network", "ls", "--filter", "plugin=bridge", "--filter", "name=" + name}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Equal(0)) + Expect(session.OutputToString()).To(ContainSubstring(name)) + }) + + It("podman network list --filter two names", func() { + name1, path1 := generateNetworkConfig(podmanTest) + defer removeConf(path1) + + name2, path2 := generateNetworkConfig(podmanTest) + defer removeConf(path2) + + session := podmanTest.Podman([]string{"network", "ls", "--filter", "name=" + name1, "--filter", "name=" + name2}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Equal(0)) + Expect(session.OutputToString()).To(ContainSubstring(name1)) + Expect(session.OutputToString()).To(ContainSubstring(name2)) + }) + + It("podman network list --filter labels", func() { + net1 := "labelnet" + stringid.GenerateNonCryptoID() + label1 := "testlabel1=abc" + label2 := "abcdef" + session := podmanTest.Podman([]string{"network", "create", "--label", label1, net1}) + session.WaitWithDefaultTimeout() + defer podmanTest.removeCNINetwork(net1) + Expect(session.ExitCode()).To(BeZero()) + + net2 := "labelnet" + stringid.GenerateNonCryptoID() + session = podmanTest.Podman([]string{"network", "create", "--label", label1, "--label", label2, net2}) + session.WaitWithDefaultTimeout() + defer podmanTest.removeCNINetwork(net2) + Expect(session.ExitCode()).To(BeZero()) + + session = podmanTest.Podman([]string{"network", "ls", "--filter", "label=" + label1}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Equal(0)) + Expect(session.OutputToString()).To(ContainSubstring(net1)) + Expect(session.OutputToString()).To(ContainSubstring(net2)) + + session = podmanTest.Podman([]string{"network", "ls", "--filter", "label=" + label1, "--filter", "label=" + label2}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Equal(0)) + Expect(session.OutputToString()).ToNot(ContainSubstring(net1)) + Expect(session.OutputToString()).To(ContainSubstring(net2)) + }) + + It("podman network list --filter invalid value", func() { + session := podmanTest.Podman([]string{"network", "ls", "--filter", "namr=ab"}) + session.WaitWithDefaultTimeout() + Expect(session).To(ExitWithError()) + Expect(session.ErrorToString()).To(ContainSubstring(`invalid filter "namr"`)) + }) + It("podman network list --filter failure", func() { name, path := generateNetworkConfig(podmanTest) defer removeConf(path) diff --git a/test/e2e/pull_test.go b/test/e2e/pull_test.go index f1b055d6d..c572fc5ac 100644 --- a/test/e2e/pull_test.go +++ b/test/e2e/pull_test.go @@ -4,6 +4,7 @@ import ( "fmt" "os" "path/filepath" + "runtime" "strings" . "github.com/containers/podman/v2/test/utils" @@ -494,4 +495,31 @@ var _ = Describe("Podman pull", func() { Expect(data[0].ID).To(Equal(image1)) } }) + + It("podman pull --platform", func() { + session := podmanTest.Podman([]string{"pull", "--platform=linux/bogus", ALPINE}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Equal(125)) + expectedError := "no image found in manifest list for architecture bogus" + Expect(session.ErrorToString()).To(ContainSubstring(expectedError)) + + session = podmanTest.Podman([]string{"pull", "--platform=linux/arm64", "--override-os", "windows", ALPINE}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Equal(125)) + expectedError = "--platform option can not be specified with --overide-arch or --override-os" + Expect(session.ErrorToString()).To(ContainSubstring(expectedError)) + + session = podmanTest.Podman([]string{"pull", "-q", "--platform=linux/arm64", ALPINE}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Equal(0)) + + setup := podmanTest.Podman([]string{"image", "inspect", session.OutputToString()}) + setup.WaitWithDefaultTimeout() + Expect(setup.ExitCode()).To(Equal(0)) + + data := setup.InspectImageJSON() // returns []inspect.ImageData + Expect(len(data)).To(Equal(1)) + Expect(data[0].Os).To(Equal(runtime.GOOS)) + Expect(data[0].Architecture).To(Equal("arm64")) + }) }) diff --git a/test/system/010-images.bats b/test/system/010-images.bats index 98bb0cc57..ee6da30ec 100644 --- a/test/system/010-images.bats +++ b/test/system/010-images.bats @@ -59,7 +59,8 @@ Labels.created_at | 20[0-9-]\\\+T[0-9:]\\\+Z @test "podman images - history output" { # podman history is persistent: it permanently alters our base image. # Create a dummy image here so we leave our setup as we found it. - run_podman run --name my-container $IMAGE true + # Multiple --name options confirm command-line override (last one wins) + run_podman run --name ignore-me --name my-container $IMAGE true run_podman commit my-container my-test-image run_podman images my-test-image --format '{{ .History }}' @@ -87,7 +88,8 @@ Labels.created_at | 20[0-9-]\\\+T[0-9:]\\\+Z } @test "podman images - filter" { - run_podman inspect --format '{{.ID}}' $IMAGE + # Multiple --format options confirm command-line override (last one wins) + run_podman inspect --format '{{.XYZ}}' --format '{{.ID}}' $IMAGE iid=$output run_podman images --noheading --filter=after=$iid diff --git a/test/system/030-run.bats b/test/system/030-run.bats index 71831da10..37695f205 100644 --- a/test/system/030-run.bats +++ b/test/system/030-run.bats @@ -449,7 +449,9 @@ json-file | f msg=$(random_string 20) pidfile="${PODMAN_TMPDIR}/$(random_string 20)" - run_podman run --name myctr --log-driver journald --conmon-pidfile $pidfile $IMAGE echo $msg + # Multiple --log-driver options to confirm that last one wins + run_podman run --name myctr --log-driver=none --log-driver journald \ + --conmon-pidfile $pidfile $IMAGE echo $msg journalctl --output cat _PID=$(cat $pidfile) is "$output" "$msg" "check that journalctl output equals the container output" @@ -464,7 +466,9 @@ json-file | f run_podman run --rm $IMAGE date -r $testfile is "$output" "Sun Sep 13 12:26:40 UTC 2020" "podman run with no TZ" - run_podman run --rm --tz=MST7MDT $IMAGE date -r $testfile + # Multiple --tz options; confirm that the last one wins + run_podman run --rm --tz=US/Eastern --tz=Iceland --tz=MST7MDT \ + $IMAGE date -r $testfile is "$output" "Sun Sep 13 06:26:40 MDT 2020" "podman run with --tz=MST7MDT" # --tz=local pays attention to /etc/localtime, not $TZ. We set TZ anyway, @@ -533,8 +537,15 @@ json-file | f } @test "podman run with --net=host and --port prints warning" { - run_podman run -d --rm -p 8080 --net=host $IMAGE ls > /dev/null - is "$output" ".*Port mappings have been discarded as one of the Host, Container, Pod, and None network modes are in use" + rand=$(random_string 10) + + # Please keep the duplicate "--net" options; this tests against #8507, + # a regression in which subsequent --net options did not override earlier. + run_podman run --rm -p 8080 --net=none --net=host $IMAGE echo $rand + is "${lines[0]}" \ + "Port mappings have been discarded as one of the Host, Container, Pod, and None network modes are in use" \ + "Warning is emitted before container output" + is "${lines[1]}" "$rand" "Container runs successfully despite warning" } # vim: filetype=sh diff --git a/test/system/040-ps.bats b/test/system/040-ps.bats index dec2df4d5..1ed2779b2 100644 --- a/test/system/040-ps.bats +++ b/test/system/040-ps.bats @@ -35,4 +35,51 @@ load helpers run_podman rm $cid } +@test "podman ps --filter" { + run_podman run -d --name runner $IMAGE top + cid_runner=$output + + run_podman run -d --name stopped $IMAGE true + cid_stopped=$output + run_podman wait stopped + + run_podman run -d --name failed $IMAGE false + cid_failed=$output + run_podman wait failed + + run_podman create --name created $IMAGE echo hi + cid_created=$output + + run_podman ps --filter name=runner --format '{{.ID}}' + is "$output" "${cid_runner:0:12}" "filter: name=runner" + + # Stopped container should not appear (because we're not using -a) + run_podman ps --filter name=stopped --format '{{.ID}}' + is "$output" "" "filter: name=stopped (without -a)" + + # Again, but with -a + run_podman ps -a --filter name=stopped --format '{{.ID}}' + is "$output" "${cid_stopped:0:12}" "filter: name=stopped (with -a)" + + run_podman ps --filter status=stopped --format '{{.Names}}' --sort names + is "${lines[0]}" "failed" "status=stopped: 1 of 2" + is "${lines[1]}" "stopped" "status=stopped: 2 of 2" + + run_podman ps --filter status=exited --filter exited=0 --format '{{.Names}}' + is "$output" "stopped" "exited=0" + + run_podman ps --filter status=exited --filter exited=1 --format '{{.Names}}' + is "$output" "failed" "exited=1" + + # Multiple statuses allowed; and test sort=created + run_podman ps -a --filter status=exited --filter status=running \ + --format '{{.Names}}' --sort created + is "${lines[0]}" "runner" "status=stopped: 1 of 3" + is "${lines[1]}" "stopped" "status=stopped: 2 of 3" + is "${lines[2]}" "failed" "status=stopped: 3 of 3" + + run_podman stop -t 1 runner + run_podman rm -a +} + # vim: filetype=sh diff --git a/test/system/070-build.bats b/test/system/070-build.bats index 83bcd13eb..59da503a6 100644 --- a/test/system/070-build.bats +++ b/test/system/070-build.bats @@ -135,10 +135,13 @@ echo "\$1" printenv | grep MYENV | sort | sed -e 's/^MYENV.=//' EOF - # For overriding with --env-file - cat >$PODMAN_TMPDIR/env-file <<EOF + # For overriding with --env-file; using multiple files confirms that + # the --env-file option is cumulative, not last-one-wins. + cat >$PODMAN_TMPDIR/env-file1 <<EOF MYENV3=$s_env3 http_proxy=http-proxy-in-env-file +EOF + cat >$PODMAN_TMPDIR/env-file2 <<EOF https_proxy=https-proxy-in-env-file EOF @@ -185,7 +188,8 @@ EOF export MYENV2="$s_env2" export MYENV3="env-file-should-override-env-host!" run_podman run --rm \ - --env-file=$PODMAN_TMPDIR/env-file \ + --env-file=$PODMAN_TMPDIR/env-file1 \ + --env-file=$PODMAN_TMPDIR/env-file2 \ ${ENVHOST} \ -e MYENV4="$s_env4" \ build_test @@ -205,7 +209,9 @@ EOF # Proxies - environment should override container, but not env-file http_proxy=http-proxy-from-env ftp_proxy=ftp-proxy-from-env \ - run_podman run --rm --env-file=$PODMAN_TMPDIR/env-file \ + run_podman run --rm \ + --env-file=$PODMAN_TMPDIR/env-file1 \ + --env-file=$PODMAN_TMPDIR/env-file2 \ build_test \ printenv http_proxy https_proxy ftp_proxy is "${lines[0]}" "http-proxy-in-env-file" "env-file overrides env" @@ -222,7 +228,8 @@ EOF is "$output" "$workdir" "pwd command in container" # Determine buildah version, so we can confirm it gets into Labels - run_podman info --format '{{ .Host.BuildahVersion }}' + # Multiple --format options confirm command-line override (last one wins) + run_podman info --format '{{.Ignore}}' --format '{{ .Host.BuildahVersion }}' is "$output" "[1-9][0-9.-]\+" ".Host.BuildahVersion is reasonable" buildah_version=$output diff --git a/test/system/075-exec.bats b/test/system/075-exec.bats index edd7dedc4..c028e16c9 100644 --- a/test/system/075-exec.bats +++ b/test/system/075-exec.bats @@ -91,7 +91,8 @@ load helpers # #6829 : add username to /etc/passwd inside container if --userns=keep-id @test "podman exec - with keep-id" { - run_podman run -d --userns=keep-id $IMAGE sh -c \ + # Multiple --userns options confirm command-line override (last one wins) + run_podman run -d --userns=private --userns=keep-id $IMAGE sh -c \ "echo READY;while [ ! -f /tmp/stop ]; do sleep 1; done" cid="$output" wait_for_ready $cid diff --git a/test/system/200-pod.bats b/test/system/200-pod.bats index b0f645c53..51835e4a3 100644 --- a/test/system/200-pod.bats +++ b/test/system/200-pod.bats @@ -286,6 +286,10 @@ EOF is "$output" "nc: bind: Address in use" \ "two containers cannot bind to same port" + # make sure we can ping; failure here might mean that capabilities are wrong + run_podman run --rm --pod mypod $IMAGE ping -c1 127.0.0.1 + run_podman run --rm --pod mypod $IMAGE ping -c1 $hostname + # While the container is still running, run 'podman ps' (no --format) # and confirm that the output includes the published port run_podman ps --filter id=$cid diff --git a/vendor/github.com/spf13/cobra/bash_completions.go b/vendor/github.com/spf13/cobra/bash_completions.go index 2ceebc552..846636d75 100644 --- a/vendor/github.com/spf13/cobra/bash_completions.go +++ b/vendor/github.com/spf13/cobra/bash_completions.go @@ -5,95 +5,70 @@ import ( "fmt" "io" "os" + "sort" + "strings" + + "github.com/spf13/pflag" ) // Annotations for Bash completion. const ( - BashCompFilenameExt = "cobra_annotation_bash_completion_filename_extensions" - // BashCompCustom should be avoided as it only works for bash. - // Function RegisterFlagCompletionFunc() should be used instead. + BashCompFilenameExt = "cobra_annotation_bash_completion_filename_extensions" BashCompCustom = "cobra_annotation_bash_completion_custom" BashCompOneRequiredFlag = "cobra_annotation_bash_completion_one_required_flag" BashCompSubdirsInDir = "cobra_annotation_bash_completion_subdirs_in_dir" ) -// GenBashCompletion generates bash completion file and writes to the passed writer. -func (c *Command) GenBashCompletion(w io.Writer) error { - return c.genBashCompletion(w, false) -} - -// GenBashCompletionWithDesc generates bash completion file with descriptions and writes to the passed writer. -func (c *Command) GenBashCompletionWithDesc(w io.Writer) error { - return c.genBashCompletion(w, true) -} - -// GenBashCompletionFile generates bash completion file. -func (c *Command) GenBashCompletionFile(filename string) error { - return c.genBashCompletionFile(filename, false) -} - -// GenBashCompletionFileWithDesc generates bash completion file with descriptions. -func (c *Command) GenBashCompletionFileWithDesc(filename string) error { - return c.genBashCompletionFile(filename, true) +func writePreamble(buf *bytes.Buffer, name string) { + buf.WriteString(fmt.Sprintf("# bash completion for %-36s -*- shell-script -*-\n", name)) + buf.WriteString(fmt.Sprintf(` +__%[1]s_debug() +{ + if [[ -n ${BASH_COMP_DEBUG_FILE} ]]; then + echo "$*" >> "${BASH_COMP_DEBUG_FILE}" + fi } -func (c *Command) genBashCompletionFile(filename string, includeDesc bool) error { - outFile, err := os.Create(filename) - if err != nil { - return err - } - defer outFile.Close() - - return c.genBashCompletion(outFile, includeDesc) +# Homebrew on Macs have version 1.3 of bash-completion which doesn't include +# _init_completion. This is a very minimal version of that function. +__%[1]s_init_completion() +{ + COMPREPLY=() + _get_comp_words_by_ref "$@" cur prev words cword } -func (c *Command) genBashCompletion(w io.Writer, includeDesc bool) error { - buf := new(bytes.Buffer) - if len(c.BashCompletionFunction) > 0 { - buf.WriteString(c.BashCompletionFunction + "\n") - } - genBashComp(buf, c.Name(), includeDesc) - - _, err := buf.WriteTo(w) - return err +__%[1]s_index_of_word() +{ + local w word=$1 + shift + index=0 + for w in "$@"; do + [[ $w = "$word" ]] && return + index=$((index+1)) + done + index=-1 } -func genBashComp(buf *bytes.Buffer, name string, includeDesc bool) { - compCmd := ShellCompRequestCmd - if !includeDesc { - compCmd = ShellCompNoDescRequestCmd - } - - buf.WriteString(fmt.Sprintf(`# bash completion for %-36[1]s -*- shell-script -*- - -__%[1]s_debug() +__%[1]s_contains_word() { - if [[ -n ${BASH_COMP_DEBUG_FILE} ]]; then - echo "$*" >> "${BASH_COMP_DEBUG_FILE}" - fi + local w word=$1; shift + for w in "$@"; do + [[ $w = "$word" ]] && return + done + return 1 } -__%[1]s_perform_completion() +__%[1]s_handle_go_custom_completion() { - __%[1]s_debug - __%[1]s_debug "========= starting completion logic ==========" - __%[1]s_debug "cur is ${cur}, words[*] is ${words[*]}, #words[@] is ${#words[@]}, cword is $cword" - - # The user could have moved the cursor backwards on the command-line. - # We need to trigger completion from the $cword location, so we need - # to truncate the command-line ($words) up to the $cword location. - words=("${words[@]:0:$cword+1}") - __%[1]s_debug "Truncated words[*]: ${words[*]}," + __%[1]s_debug "${FUNCNAME[0]}: cur is ${cur}, words[*] is ${words[*]}, #words[@] is ${#words[@]}" local shellCompDirectiveError=%[3]d local shellCompDirectiveNoSpace=%[4]d local shellCompDirectiveNoFileComp=%[5]d local shellCompDirectiveFilterFileExt=%[6]d local shellCompDirectiveFilterDirs=%[7]d - local shellCompDirectiveLegacyCustomComp=%[8]d - local shellCompDirectiveLegacyCustomArgsComp=%[9]d - local out requestComp lastParam lastChar comp directive args flagPrefix + local out requestComp lastParam lastChar comp directive args # Prepare the command to request completions for the program. # Calling ${words[0]} instead of directly %[1]s allows to handle aliases @@ -102,24 +77,16 @@ __%[1]s_perform_completion() lastParam=${words[$((${#words[@]}-1))]} lastChar=${lastParam:$((${#lastParam}-1)):1} - __%[1]s_debug "lastParam ${lastParam}, lastChar ${lastChar}" + __%[1]s_debug "${FUNCNAME[0]}: lastParam ${lastParam}, lastChar ${lastChar}" if [ -z "${cur}" ] && [ "${lastChar}" != "=" ]; then # If the last parameter is complete (there is a space following it) # We add an extra empty parameter so we can indicate this to the go method. - __%[1]s_debug "Adding extra empty parameter" + __%[1]s_debug "${FUNCNAME[0]}: Adding extra empty parameter" requestComp="${requestComp} \"\"" fi - # When completing a flag with an = (e.g., %[1]s -n=<TAB>) - # bash focuses on the part after the =, so we need to remove - # the flag part from $cur - if [[ "${cur}" == -*=* ]]; then - flagPrefix="${cur%%%%=*}=" - cur="${cur#*=}" - fi - - __%[1]s_debug "Calling ${requestComp}" + __%[1]s_debug "${FUNCNAME[0]}: calling ${requestComp}" # Use eval to handle any environment variables and such out=$(eval "${requestComp}" 2>/dev/null) @@ -131,23 +98,23 @@ __%[1]s_perform_completion() # There is not directive specified directive=0 fi - __%[1]s_debug "The completion directive is: ${directive}" - __%[1]s_debug "The completions are: ${out[*]}" + __%[1]s_debug "${FUNCNAME[0]}: the completion directive is: ${directive}" + __%[1]s_debug "${FUNCNAME[0]}: the completions are: ${out[*]}" if [ $((directive & shellCompDirectiveError)) -ne 0 ]; then # Error code. No completion. - __%[1]s_debug "Received error from custom completion go code" + __%[1]s_debug "${FUNCNAME[0]}: received error from custom completion go code" return else if [ $((directive & shellCompDirectiveNoSpace)) -ne 0 ]; then if [[ $(type -t compopt) = "builtin" ]]; then - __%[1]s_debug "Activating no space" + __%[1]s_debug "${FUNCNAME[0]}: activating no space" compopt -o nospace fi fi if [ $((directive & shellCompDirectiveNoFileComp)) -ne 0 ]; then if [[ $(type -t compopt) = "builtin" ]]; then - __%[1]s_debug "Activating no file completion" + __%[1]s_debug "${FUNCNAME[0]}: activating no file completion" compopt +o default fi fi @@ -156,7 +123,6 @@ __%[1]s_perform_completion() if [ $((directive & shellCompDirectiveFilterFileExt)) -ne 0 ]; then # File extension filtering local fullFilter filter filteringCmd - # Do not use quotes around the $out variable or else newline # characters will be kept. for filter in ${out[*]}; do @@ -168,173 +134,545 @@ __%[1]s_perform_completion() $filteringCmd elif [ $((directive & shellCompDirectiveFilterDirs)) -ne 0 ]; then # File completion for directories only - + local subDir # Use printf to strip any trailing newline - local subdir subdir=$(printf "%%s" "${out[0]}") if [ -n "$subdir" ]; then __%[1]s_debug "Listing directories in $subdir" - pushd "$subdir" >/dev/null 2>&1 && _filedir -d && popd >/dev/null 2>&1 || return + __%[1]s_handle_subdirs_in_dir_flag "$subdir" else __%[1]s_debug "Listing directories in ." _filedir -d fi - elif [ $((directive & shellCompDirectiveLegacyCustomComp)) -ne 0 ]; then - local cmd - __%[1]s_debug "Legacy custom completion. Directive: $directive, cmds: ${out[*]}" - - # The following variables should get their value through the commands - # we have received as completions and are parsing below. - local last_command - local nouns - - # Execute every command received - while IFS='' read -r cmd; do - __%[1]s_debug "About to execute: $cmd" - eval "$cmd" - done < <(printf "%%s\n" "${out[@]}") - - __%[1]s_debug "last_command: $last_command" - __%[1]s_debug "nouns[0]: ${nouns[0]}, nouns[1]: ${nouns[1]}" - - if [ $((directive & shellCompDirectiveLegacyCustomArgsComp)) -ne 0 ]; then - # We should call the global legacy custom completion function, if it is defined - if declare -F __%[1]s_custom_func >/dev/null; then - # Use command name qualified legacy custom func - __%[1]s_debug "About to call: __%[1]s_custom_func" - __%[1]s_custom_func - elif declare -F __custom_func >/dev/null; then - # Otherwise fall back to unqualified legacy custom func for compatibility - __%[1]s_debug "About to call: __custom_func" - __custom_func - fi - fi else - local tab - tab=$(printf '\t') - local longest=0 - # Look for the longest completion so that we can format things nicely while IFS='' read -r comp; do - comp=${comp%%%%$tab*} - if ((${#comp}>longest)); then - longest=${#comp} + COMPREPLY+=("$comp") + done < <(compgen -W "${out[*]}" -- "$cur") + fi +} + +__%[1]s_handle_reply() +{ + __%[1]s_debug "${FUNCNAME[0]}" + local comp + case $cur in + -*) + if [[ $(type -t compopt) = "builtin" ]]; then + compopt -o nospace + fi + local allflags + if [ ${#must_have_one_flag[@]} -ne 0 ]; then + allflags=("${must_have_one_flag[@]}") + else + allflags=("${flags[*]} ${two_word_flags[*]}") + fi + while IFS='' read -r comp; do + COMPREPLY+=("$comp") + done < <(compgen -W "${allflags[*]}" -- "$cur") + if [[ $(type -t compopt) = "builtin" ]]; then + [[ "${COMPREPLY[0]}" == *= ]] || compopt +o nospace fi - done < <(printf "%%s\n" "${out[@]}") - local completions=() - while IFS='' read -r comp; do - if [ -z "$comp" ]; then - continue + # complete after --flag=abc + if [[ $cur == *=* ]]; then + if [[ $(type -t compopt) = "builtin" ]]; then + compopt +o nospace + fi + + local index flag + flag="${cur%%=*}" + __%[1]s_index_of_word "${flag}" "${flags_with_completion[@]}" + COMPREPLY=() + if [[ ${index} -ge 0 ]]; then + PREFIX="" + cur="${cur#*=}" + ${flags_completion[${index}]} + if [ -n "${ZSH_VERSION}" ]; then + # zsh completion needs --flag= prefix + eval "COMPREPLY=( \"\${COMPREPLY[@]/#/${flag}=}\" )" + fi + fi fi + return 0; + ;; + esac + + # check if we are handling a flag with special work handling + local index + __%[1]s_index_of_word "${prev}" "${flags_with_completion[@]}" + if [[ ${index} -ge 0 ]]; then + ${flags_completion[${index}]} + return + fi - __%[1]s_debug "Original comp: $comp" - comp="$(__%[1]s_format_comp_descriptions "$comp" "$longest")" - __%[1]s_debug "Final comp: $comp" - completions+=("$comp") - done < <(printf "%%s\n" "${out[@]}") + # we are parsing a flag and don't have a special handler, no completion + if [[ ${cur} != "${words[cword]}" ]]; then + return + fi + local completions + completions=("${commands[@]}") + if [[ ${#must_have_one_noun[@]} -ne 0 ]]; then + completions+=("${must_have_one_noun[@]}") + elif [[ -n "${has_completion_function}" ]]; then + # if a go completion function is provided, defer to that function + __%[1]s_handle_go_custom_completion + fi + if [[ ${#must_have_one_flag[@]} -ne 0 ]]; then + completions+=("${must_have_one_flag[@]}") + fi + while IFS='' read -r comp; do + COMPREPLY+=("$comp") + done < <(compgen -W "${completions[*]}" -- "$cur") + + if [[ ${#COMPREPLY[@]} -eq 0 && ${#noun_aliases[@]} -gt 0 && ${#must_have_one_noun[@]} -ne 0 ]]; then while IFS='' read -r comp; do - # Although this script should only be used for bash - # there may be programs that still convert the bash - # script into a zsh one. To continue supporting those - # programs, we do this single adaptation for zsh - if [ -n "${ZSH_VERSION}" ]; then - # zsh completion needs --flag= prefix - COMPREPLY+=("$flagPrefix$comp") - else - COMPREPLY+=("$comp") - fi - done < <(compgen -W "${completions[*]}" -- "$cur") - - # If there is a single completion left, remove the description text - if [ ${#COMPREPLY[*]} -eq 1 ]; then - __%[1]s_debug "COMPREPLY[0]: ${COMPREPLY[0]}" - comp="${COMPREPLY[0]%%%% *}" - __%[1]s_debug "Removed description from single completion, which is now: ${comp}" - COMPREPLY=() COMPREPLY+=("$comp") + done < <(compgen -W "${noun_aliases[*]}" -- "$cur") + fi + + if [[ ${#COMPREPLY[@]} -eq 0 ]]; then + if declare -F __%[1]s_custom_func >/dev/null; then + # try command name qualified custom func + __%[1]s_custom_func + else + # otherwise fall back to unqualified for compatibility + declare -F __custom_func >/dev/null && __custom_func + fi + fi + + # available in bash-completion >= 2, not always present on macOS + if declare -F __ltrim_colon_completions >/dev/null; then + __ltrim_colon_completions "$cur" + fi + + # If there is only 1 completion and it is a flag with an = it will be completed + # but we don't want a space after the = + if [[ "${#COMPREPLY[@]}" -eq "1" ]] && [[ $(type -t compopt) = "builtin" ]] && [[ "${COMPREPLY[0]}" == --*= ]]; then + compopt -o nospace + fi +} + +# The arguments should be in the form "ext1|ext2|extn" +__%[1]s_handle_filename_extension_flag() +{ + local ext="$1" + _filedir "@(${ext})" +} + +__%[1]s_handle_subdirs_in_dir_flag() +{ + local dir="$1" + pushd "${dir}" >/dev/null 2>&1 && _filedir -d && popd >/dev/null 2>&1 || return +} + +__%[1]s_handle_flag() +{ + __%[1]s_debug "${FUNCNAME[0]}: c is $c words[c] is ${words[c]}" + + # if a command required a flag, and we found it, unset must_have_one_flag() + local flagname=${words[c]} + local flagvalue + # if the word contained an = + if [[ ${words[c]} == *"="* ]]; then + flagvalue=${flagname#*=} # take in as flagvalue after the = + flagname=${flagname%%=*} # strip everything after the = + flagname="${flagname}=" # but put the = back + fi + __%[1]s_debug "${FUNCNAME[0]}: looking for ${flagname}" + if __%[1]s_contains_word "${flagname}" "${must_have_one_flag[@]}"; then + must_have_one_flag=() + fi + + # if you set a flag which only applies to this command, don't show subcommands + if __%[1]s_contains_word "${flagname}" "${local_nonpersistent_flags[@]}"; then + commands=() + fi + + # keep flag value with flagname as flaghash + # flaghash variable is an associative array which is only supported in bash > 3. + if [[ -z "${BASH_VERSION}" || "${BASH_VERSINFO[0]}" -gt 3 ]]; then + if [ -n "${flagvalue}" ] ; then + flaghash[${flagname}]=${flagvalue} + elif [ -n "${words[ $((c+1)) ]}" ] ; then + flaghash[${flagname}]=${words[ $((c+1)) ]} + else + flaghash[${flagname}]="true" # pad "true" for bool flag + fi + fi + + # skip the argument to a two word flag + if [[ ${words[c]} != *"="* ]] && __%[1]s_contains_word "${words[c]}" "${two_word_flags[@]}"; then + __%[1]s_debug "${FUNCNAME[0]}: found a flag ${words[c]}, skip the next argument" + c=$((c+1)) + # if we are looking for a flags value, don't show commands + if [[ $c -eq $cword ]]; then + commands=() fi fi - __%[1]s_handle_special_char "$cur" : - __%[1]s_handle_special_char "$cur" = + c=$((c+1)) + } -__%[1]s_handle_special_char() +__%[1]s_handle_noun() { - local comp="$1" - local char=$2 - if [[ "$comp" == *${char}* && "$COMP_WORDBREAKS" == *${char}* ]]; then - local word=${comp%%"${comp##*${char}}"} - local idx=${#COMPREPLY[*]} - while [[ $((--idx)) -ge 0 ]]; do - COMPREPLY[$idx]=${COMPREPLY[$idx]#"$word"} - done + __%[1]s_debug "${FUNCNAME[0]}: c is $c words[c] is ${words[c]}" + + if __%[1]s_contains_word "${words[c]}" "${must_have_one_noun[@]}"; then + must_have_one_noun=() + elif __%[1]s_contains_word "${words[c]}" "${noun_aliases[@]}"; then + must_have_one_noun=() fi + + nouns+=("${words[c]}") + c=$((c+1)) } -__%[1]s_format_comp_descriptions() +__%[1]s_handle_command() { - local tab - tab=$(printf '\t') - local comp="$1" - local longest=$2 - - # Properly format the description string which follows a tab character if there is one - if [[ "$comp" == *$tab* ]]; then - desc=${comp#*$tab} - comp=${comp%%%%$tab*} - - # $COLUMNS stores the current shell width. - # Remove an extra 4 because we add 2 spaces and 2 parentheses. - maxdesclength=$(( COLUMNS - longest - 4 )) - - # Make sure we can fit a description of at least 8 characters - # if we are to align the descriptions. - if [[ $maxdesclength -gt 8 ]]; then - # Add the proper number of spaces to align the descriptions - for ((i = ${#comp} ; i < longest ; i++)); do - comp+=" " - done + __%[1]s_debug "${FUNCNAME[0]}: c is $c words[c] is ${words[c]}" + + local next_command + if [[ -n ${last_command} ]]; then + next_command="_${last_command}_${words[c]//:/__}" + else + if [[ $c -eq 0 ]]; then + next_command="_%[1]s_root_command" else - # Don't pad the descriptions so we can fit more text after the completion - maxdesclength=$(( COLUMNS - ${#comp} - 4 )) + next_command="_${words[c]//:/__}" fi + fi + c=$((c+1)) + __%[1]s_debug "${FUNCNAME[0]}: looking for ${next_command}" + declare -F "$next_command" >/dev/null && $next_command +} - # If there is enough space for any description text, - # truncate the descriptions that are too long for the shell width - if [ $maxdesclength -gt 0 ]; then - if [ ${#desc} -gt $maxdesclength ]; then - desc=${desc:0:$(( maxdesclength - 1 ))} - desc+="…" - fi - comp+=" ($desc)" +__%[1]s_handle_word() +{ + if [[ $c -ge $cword ]]; then + __%[1]s_handle_reply + return + fi + __%[1]s_debug "${FUNCNAME[0]}: c is $c words[c] is ${words[c]}" + if [[ "${words[c]}" == -* ]]; then + __%[1]s_handle_flag + elif __%[1]s_contains_word "${words[c]}" "${commands[@]}"; then + __%[1]s_handle_command + elif [[ $c -eq 0 ]]; then + __%[1]s_handle_command + elif __%[1]s_contains_word "${words[c]}" "${command_aliases[@]}"; then + # aliashash variable is an associative array which is only supported in bash > 3. + if [[ -z "${BASH_VERSION}" || "${BASH_VERSINFO[0]}" -gt 3 ]]; then + words[c]=${aliashash[${words[c]}]} + __%[1]s_handle_command + else + __%[1]s_handle_noun fi + else + __%[1]s_handle_noun fi + __%[1]s_handle_word +} - # Must use printf to escape all special characters - printf "%%q" "${comp}" +`, name, ShellCompNoDescRequestCmd, + ShellCompDirectiveError, ShellCompDirectiveNoSpace, ShellCompDirectiveNoFileComp, + ShellCompDirectiveFilterFileExt, ShellCompDirectiveFilterDirs)) } -__start_%[1]s() -{ +func writePostscript(buf *bytes.Buffer, name string) { + name = strings.Replace(name, ":", "__", -1) + buf.WriteString(fmt.Sprintf("__start_%s()\n", name)) + buf.WriteString(fmt.Sprintf(`{ local cur prev words cword + declare -A flaghash 2>/dev/null || : + declare -A aliashash 2>/dev/null || : + if declare -F _init_completion >/dev/null 2>&1; then + _init_completion -s || return + else + __%[1]s_init_completion -n "=" || return + fi - COMPREPLY=() - _get_comp_words_by_ref -n "=:" cur prev words cword - - __%[1]s_perform_completion + local c=0 + local flags=() + local two_word_flags=() + local local_nonpersistent_flags=() + local flags_with_completion=() + local flags_completion=() + local commands=("%[1]s") + local must_have_one_flag=() + local must_have_one_noun=() + local has_completion_function + local last_command + local nouns=() + + __%[1]s_handle_word } -if [[ $(type -t compopt) = "builtin" ]]; then - complete -o default -F __start_%[1]s %[1]s +`, name)) + buf.WriteString(fmt.Sprintf(`if [[ $(type -t compopt) = "builtin" ]]; then + complete -o default -F __start_%s %s else - complete -o default -o nospace -F __start_%[1]s %[1]s + complete -o default -o nospace -F __start_%s %s fi -# ex: ts=4 sw=4 et filetype=sh -`, name, compCmd, - ShellCompDirectiveError, ShellCompDirectiveNoSpace, ShellCompDirectiveNoFileComp, - ShellCompDirectiveFilterFileExt, ShellCompDirectiveFilterDirs, - shellCompDirectiveLegacyCustomComp, shellCompDirectiveLegacyCustomArgsComp)) +`, name, name, name, name)) + buf.WriteString("# ex: ts=4 sw=4 et filetype=sh\n") +} + +func writeCommands(buf *bytes.Buffer, cmd *Command) { + buf.WriteString(" commands=()\n") + for _, c := range cmd.Commands() { + if !c.IsAvailableCommand() && c != cmd.helpCommand { + continue + } + buf.WriteString(fmt.Sprintf(" commands+=(%q)\n", c.Name())) + writeCmdAliases(buf, c) + } + buf.WriteString("\n") +} + +func writeFlagHandler(buf *bytes.Buffer, name string, annotations map[string][]string, cmd *Command) { + for key, value := range annotations { + switch key { + case BashCompFilenameExt: + buf.WriteString(fmt.Sprintf(" flags_with_completion+=(%q)\n", name)) + + var ext string + if len(value) > 0 { + ext = fmt.Sprintf("__%s_handle_filename_extension_flag ", cmd.Root().Name()) + strings.Join(value, "|") + } else { + ext = "_filedir" + } + buf.WriteString(fmt.Sprintf(" flags_completion+=(%q)\n", ext)) + case BashCompCustom: + buf.WriteString(fmt.Sprintf(" flags_with_completion+=(%q)\n", name)) + if len(value) > 0 { + handlers := strings.Join(value, "; ") + buf.WriteString(fmt.Sprintf(" flags_completion+=(%q)\n", handlers)) + } else { + buf.WriteString(" flags_completion+=(:)\n") + } + case BashCompSubdirsInDir: + buf.WriteString(fmt.Sprintf(" flags_with_completion+=(%q)\n", name)) + + var ext string + if len(value) == 1 { + ext = fmt.Sprintf("__%s_handle_subdirs_in_dir_flag ", cmd.Root().Name()) + value[0] + } else { + ext = "_filedir -d" + } + buf.WriteString(fmt.Sprintf(" flags_completion+=(%q)\n", ext)) + } + } +} + +func writeShortFlag(buf *bytes.Buffer, flag *pflag.Flag, cmd *Command) { + name := flag.Shorthand + format := " " + if len(flag.NoOptDefVal) == 0 { + format += "two_word_" + } + format += "flags+=(\"-%s\")\n" + buf.WriteString(fmt.Sprintf(format, name)) + writeFlagHandler(buf, "-"+name, flag.Annotations, cmd) +} + +func writeFlag(buf *bytes.Buffer, flag *pflag.Flag, cmd *Command) { + name := flag.Name + format := " flags+=(\"--%s" + if len(flag.NoOptDefVal) == 0 { + format += "=" + } + format += "\")\n" + buf.WriteString(fmt.Sprintf(format, name)) + if len(flag.NoOptDefVal) == 0 { + format = " two_word_flags+=(\"--%s\")\n" + buf.WriteString(fmt.Sprintf(format, name)) + } + writeFlagHandler(buf, "--"+name, flag.Annotations, cmd) +} + +func writeLocalNonPersistentFlag(buf *bytes.Buffer, flag *pflag.Flag) { + name := flag.Name + format := " local_nonpersistent_flags+=(\"--%[1]s\")\n" + if len(flag.NoOptDefVal) == 0 { + format += " local_nonpersistent_flags+=(\"--%[1]s=\")\n" + } + buf.WriteString(fmt.Sprintf(format, name)) + if len(flag.Shorthand) > 0 { + buf.WriteString(fmt.Sprintf(" local_nonpersistent_flags+=(\"-%s\")\n", flag.Shorthand)) + } +} + +// Setup annotations for go completions for registered flags +func prepareCustomAnnotationsForFlags(cmd *Command) { + for flag := range flagCompletionFunctions { + // Make sure the completion script calls the __*_go_custom_completion function for + // every registered flag. We need to do this here (and not when the flag was registered + // for completion) so that we can know the root command name for the prefix + // of __<prefix>_go_custom_completion + if flag.Annotations == nil { + flag.Annotations = map[string][]string{} + } + flag.Annotations[BashCompCustom] = []string{fmt.Sprintf("__%[1]s_handle_go_custom_completion", cmd.Root().Name())} + } +} + +func writeFlags(buf *bytes.Buffer, cmd *Command) { + prepareCustomAnnotationsForFlags(cmd) + buf.WriteString(` flags=() + two_word_flags=() + local_nonpersistent_flags=() + flags_with_completion=() + flags_completion=() + +`) + localNonPersistentFlags := cmd.LocalNonPersistentFlags() + cmd.NonInheritedFlags().VisitAll(func(flag *pflag.Flag) { + if nonCompletableFlag(flag) { + return + } + writeFlag(buf, flag, cmd) + if len(flag.Shorthand) > 0 { + writeShortFlag(buf, flag, cmd) + } + // localNonPersistentFlags are used to stop the completion of subcommands when one is set + // if TraverseChildren is true we should allow to complete subcommands + if localNonPersistentFlags.Lookup(flag.Name) != nil && !cmd.Root().TraverseChildren { + writeLocalNonPersistentFlag(buf, flag) + } + }) + cmd.InheritedFlags().VisitAll(func(flag *pflag.Flag) { + if nonCompletableFlag(flag) { + return + } + writeFlag(buf, flag, cmd) + if len(flag.Shorthand) > 0 { + writeShortFlag(buf, flag, cmd) + } + }) + + buf.WriteString("\n") +} + +func writeRequiredFlag(buf *bytes.Buffer, cmd *Command) { + buf.WriteString(" must_have_one_flag=()\n") + flags := cmd.NonInheritedFlags() + flags.VisitAll(func(flag *pflag.Flag) { + if nonCompletableFlag(flag) { + return + } + for key := range flag.Annotations { + switch key { + case BashCompOneRequiredFlag: + format := " must_have_one_flag+=(\"--%s" + if flag.Value.Type() != "bool" { + format += "=" + } + format += "\")\n" + buf.WriteString(fmt.Sprintf(format, flag.Name)) + + if len(flag.Shorthand) > 0 { + buf.WriteString(fmt.Sprintf(" must_have_one_flag+=(\"-%s\")\n", flag.Shorthand)) + } + } + } + }) +} + +func writeRequiredNouns(buf *bytes.Buffer, cmd *Command) { + buf.WriteString(" must_have_one_noun=()\n") + sort.Sort(sort.StringSlice(cmd.ValidArgs)) + for _, value := range cmd.ValidArgs { + // Remove any description that may be included following a tab character. + // Descriptions are not supported by bash completion. + value = strings.Split(value, "\t")[0] + buf.WriteString(fmt.Sprintf(" must_have_one_noun+=(%q)\n", value)) + } + if cmd.ValidArgsFunction != nil { + buf.WriteString(" has_completion_function=1\n") + } +} + +func writeCmdAliases(buf *bytes.Buffer, cmd *Command) { + if len(cmd.Aliases) == 0 { + return + } + + sort.Sort(sort.StringSlice(cmd.Aliases)) + + buf.WriteString(fmt.Sprint(` if [[ -z "${BASH_VERSION}" || "${BASH_VERSINFO[0]}" -gt 3 ]]; then`, "\n")) + for _, value := range cmd.Aliases { + buf.WriteString(fmt.Sprintf(" command_aliases+=(%q)\n", value)) + buf.WriteString(fmt.Sprintf(" aliashash[%q]=%q\n", value, cmd.Name())) + } + buf.WriteString(` fi`) + buf.WriteString("\n") +} +func writeArgAliases(buf *bytes.Buffer, cmd *Command) { + buf.WriteString(" noun_aliases=()\n") + sort.Sort(sort.StringSlice(cmd.ArgAliases)) + for _, value := range cmd.ArgAliases { + buf.WriteString(fmt.Sprintf(" noun_aliases+=(%q)\n", value)) + } +} + +func gen(buf *bytes.Buffer, cmd *Command) { + for _, c := range cmd.Commands() { + if !c.IsAvailableCommand() && c != cmd.helpCommand { + continue + } + gen(buf, c) + } + commandName := cmd.CommandPath() + commandName = strings.Replace(commandName, " ", "_", -1) + commandName = strings.Replace(commandName, ":", "__", -1) + + if cmd.Root() == cmd { + buf.WriteString(fmt.Sprintf("_%s_root_command()\n{\n", commandName)) + } else { + buf.WriteString(fmt.Sprintf("_%s()\n{\n", commandName)) + } + + buf.WriteString(fmt.Sprintf(" last_command=%q\n", commandName)) + buf.WriteString("\n") + buf.WriteString(" command_aliases=()\n") + buf.WriteString("\n") + + writeCommands(buf, cmd) + writeFlags(buf, cmd) + writeRequiredFlag(buf, cmd) + writeRequiredNouns(buf, cmd) + writeArgAliases(buf, cmd) + buf.WriteString("}\n\n") +} + +// GenBashCompletion generates bash completion file and writes to the passed writer. +func (c *Command) GenBashCompletion(w io.Writer) error { + buf := new(bytes.Buffer) + writePreamble(buf, c.Name()) + if len(c.BashCompletionFunction) > 0 { + buf.WriteString(c.BashCompletionFunction + "\n") + } + gen(buf, c) + writePostscript(buf, c.Name()) + + _, err := buf.WriteTo(w) + return err +} + +func nonCompletableFlag(flag *pflag.Flag) bool { + return flag.Hidden || len(flag.Deprecated) > 0 +} + +// GenBashCompletionFile generates bash completion file. +func (c *Command) GenBashCompletionFile(filename string) error { + outFile, err := os.Create(filename) + if err != nil { + return err + } + defer outFile.Close() + + return c.GenBashCompletion(outFile) } diff --git a/vendor/github.com/spf13/cobra/custom_completions.go b/vendor/github.com/spf13/cobra/custom_completions.go index e2c68ae1e..f9e88e081 100644 --- a/vendor/github.com/spf13/cobra/custom_completions.go +++ b/vendor/github.com/spf13/cobra/custom_completions.go @@ -51,11 +51,6 @@ const ( // obtain the same behavior but only for flags. ShellCompDirectiveFilterDirs - // For internal use only. - // Used to maintain backwards-compatibility with the legacy bash custom completions. - shellCompDirectiveLegacyCustomComp - shellCompDirectiveLegacyCustomArgsComp - // =========================================================================== // All directives using iota should be above this one. @@ -99,12 +94,6 @@ func (d ShellCompDirective) string() string { if d&ShellCompDirectiveFilterDirs != 0 { directives = append(directives, "ShellCompDirectiveFilterDirs") } - if d&shellCompDirectiveLegacyCustomComp != 0 { - directives = append(directives, "shellCompDirectiveLegacyCustomComp") - } - if d&shellCompDirectiveLegacyCustomArgsComp != 0 { - directives = append(directives, "shellCompDirectiveLegacyCustomArgsComp") - } if len(directives) == 0 { directives = append(directives, "ShellCompDirectiveDefault") } @@ -160,6 +149,10 @@ func (c *Command) initCompleteCmd(args []string) { fmt.Fprintln(finalCmd.OutOrStdout(), comp) } + if directive >= shellCompDirectiveMaxValue { + directive = ShellCompDirectiveDefault + } + // As the last printout, print the completion directive for the completion script to parse. // The directive integer must be that last character following a single colon (:). // The completion script expects :<directive> @@ -369,10 +362,6 @@ func (c *Command) getCompletions(args []string) (*Command, []string, ShellCompDi var comps []string comps, directive = completionFn(finalCmd, finalArgs, toComplete) completions = append(completions, comps...) - } else { - // If there is no Go custom completion defined, check for legacy bash - // custom completion to preserve backwards-compatibility - completions, directive = checkLegacyCustomCompletion(finalCmd, finalArgs, flag, completions, directive) } return finalCmd, completions, directive, nil @@ -453,16 +442,7 @@ func checkIfFlagCompletion(finalCmd *Command, args []string, lastArg string) (*p if len(lastArg) > 0 && lastArg[0] == '-' { if index := strings.Index(lastArg, "="); index >= 0 { // Flag with an = - if strings.HasPrefix(lastArg[:index], "--") { - // Flag has full name - flagName = lastArg[2:index] - } else { - // Flag is shorthand - // We have to get the last shorthand flag name - // e.g. `-asd` => d to provide the correct completion - // https://github.com/spf13/cobra/issues/1257 - flagName = lastArg[index-1 : index] - } + flagName = strings.TrimLeft(lastArg[:index], "-") lastArg = lastArg[index+1:] flagWithEqual = true } else { @@ -479,16 +459,8 @@ func checkIfFlagCompletion(finalCmd *Command, args []string, lastArg string) (*p // If the flag contains an = it means it has already been fully processed, // so we don't need to deal with it here. if index := strings.Index(prevArg, "="); index < 0 { - if strings.HasPrefix(prevArg, "--") { - // Flag has full name - flagName = prevArg[2:] - } else { - // Flag is shorthand - // We have to get the last shorthand flag name - // e.g. `-asd` => d to provide the correct completion - // https://github.com/spf13/cobra/issues/1257 - flagName = prevArg[len(prevArg)-1:] - } + flagName = strings.TrimLeft(prevArg, "-") + // Remove the uncompleted flag or else there could be an error created // for an invalid value for that flag trimmedArgs = args[:len(args)-1] @@ -541,65 +513,6 @@ func findFlag(cmd *Command, name string) *pflag.Flag { return cmd.Flag(name) } -func nonCompletableFlag(flag *pflag.Flag) bool { - return flag.Hidden || len(flag.Deprecated) > 0 -} - -// This function checks if legacy bash custom completion should be performed and if so, -// it provides the shell script with the necessary information. -func checkLegacyCustomCompletion(cmd *Command, args []string, flag *pflag.Flag, completions []string, directive ShellCompDirective) ([]string, ShellCompDirective) { - // Check if any legacy custom completion is defined for the program - if len(cmd.Root().BashCompletionFunction) > 0 { - // Legacy custom completion is only triggered if no other completions were found. - if len(completions) == 0 { - if flag != nil { - // For legacy custom flag completion, we must let the script know the bash - // functions it should call based on the content of the annotation BashCompCustom. - if values, present := flag.Annotations[BashCompCustom]; present { - if len(values) > 0 { - handlers := strings.Join(values, "; ") - // We send the commands to set the shell variables that are needed - // for legacy custom completions followed by the functions to call - // to perform the actual flag completion - completions = append(prepareLegacyCustomCompletionVars(cmd, args), handlers) - directive = directive | shellCompDirectiveLegacyCustomComp - } - } - } else { - // Check if the legacy custom_func is defined. - // This check will work for both "__custom_func" and "__<program>_custom_func". - // This could happen if the program defined some functions for legacy flag completion - // but not the legacy custom_func. - if strings.Contains(cmd.Root().BashCompletionFunction, "_custom_func") { - // For legacy args completion, the script already knows what to call - // so we only need to tell it the commands to set the shell variables needed - completions = prepareLegacyCustomCompletionVars(cmd, args) - directive = directive | shellCompDirectiveLegacyCustomComp | shellCompDirectiveLegacyCustomArgsComp - } - } - } - } - return completions, directive -} - -// The original bash completion script had some shell variables that are used by legacy bash -// custom completions. Let's set those variables to allow those legacy custom completions -// to continue working. -func prepareLegacyCustomCompletionVars(cmd *Command, args []string) []string { - var compVarCmds []string - - // "last_command" variable - commandName := cmd.CommandPath() - commandName = strings.Replace(commandName, " ", "_", -1) - commandName = strings.Replace(commandName, ":", "__", -1) - compVarCmds = append(compVarCmds, fmt.Sprintf("last_command=%s", commandName)) - - // "nouns" array variable - compVarCmds = append(compVarCmds, fmt.Sprintf("nouns=(%s)", strings.Join(args, " "))) - - return compVarCmds -} - // CompDebug prints the specified string to the same file as where the // completion script prints its logs. // Note that completion printouts should never be on stdout as they would diff --git a/vendor/github.com/spf13/cobra/fish_completions.go b/vendor/github.com/spf13/cobra/fish_completions.go index aeaef0087..eaae9bca8 100644 --- a/vendor/github.com/spf13/cobra/fish_completions.go +++ b/vendor/github.com/spf13/cobra/fish_completions.go @@ -28,9 +28,9 @@ function __%[1]s_debug end function __%[1]s_perform_completion - __%[1]s_debug "Starting __%[1]s_perform_completion" + __%[1]s_debug "Starting __%[1]s_perform_completion with: $argv" - set args (string split -- " " (commandline -c)) + set args (string split -- " " "$argv") set lastArg "$args[-1]" __%[1]s_debug "args: $args" @@ -71,22 +71,31 @@ function __%[1]s_perform_completion printf "%%s\n" "$directiveLine" end -# This function does two things: -# - Obtain the completions and store them in the global __%[1]s_comp_results -# - Return false if file completion should be performed +# This function does three things: +# 1- Obtain the completions and store them in the global __%[1]s_comp_results +# 2- Set the __%[1]s_comp_do_file_comp flag if file completion should be performed +# and unset it otherwise +# 3- Return true if the completion results are not empty function __%[1]s_prepare_completions - __%[1]s_debug "" - __%[1]s_debug "========= starting completion logic ==========" - # Start fresh + set --erase __%[1]s_comp_do_file_comp set --erase __%[1]s_comp_results - set results (__%[1]s_perform_completion) + # Check if the command-line is already provided. This is useful for testing. + if not set --query __%[1]s_comp_commandLine + # Use the -c flag to allow for completion in the middle of the line + set __%[1]s_comp_commandLine (commandline -c) + end + __%[1]s_debug "commandLine is: $__%[1]s_comp_commandLine" + + set results (__%[1]s_perform_completion "$__%[1]s_comp_commandLine") + set --erase __%[1]s_comp_commandLine __%[1]s_debug "Completion results: $results" if test -z "$results" __%[1]s_debug "No completion, probably due to a failure" # Might as well do file completion, in case it helps + set --global __%[1]s_comp_do_file_comp 1 return 1 end @@ -101,8 +110,6 @@ function __%[1]s_prepare_completions set shellCompDirectiveNoFileComp %[6]d set shellCompDirectiveFilterFileExt %[7]d set shellCompDirectiveFilterDirs %[8]d - set shellCompDirectiveLegacyCustomComp %[9]d - set shellCompDirectiveLegacyCustomArgsComp %[10]d if test -z "$directive" set directive 0 @@ -112,14 +119,6 @@ function __%[1]s_prepare_completions if test $compErr -eq 1 __%[1]s_debug "Received error directive: aborting." # Might as well do file completion, in case it helps - return 1 - end - - set legacyCustom (math (math --scale 0 $directive / $shellCompDirectiveLegacyCustomComp) %% 2) - set legacyCustomArgs (math (math --scale 0 $directive / $shellCompDirectiveLegacyCustomArgsComp) %% 2) - if test $legacyCustom -eq 1; or test $legacyCustomArgs -eq 1 - __%[1]s_debug "Legacy bash custom completion not applicable to fish" - # Do full file completion instead set --global __%[1]s_comp_do_file_comp 1 return 1 end @@ -129,6 +128,7 @@ function __%[1]s_prepare_completions if test $filefilter -eq 1; or test $dirfilter -eq 1 __%[1]s_debug "File extension filtering or directory filtering not supported" # Do full file completion instead + set --global __%[1]s_comp_do_file_comp 1 return 1 end @@ -137,51 +137,27 @@ function __%[1]s_prepare_completions __%[1]s_debug "nospace: $nospace, nofiles: $nofiles" - # If we want to prevent a space, or if file completion is NOT disabled, - # we need to count the number of valid completions. - # To do so, we will filter on prefix as the completions we have received - # may not already be filtered so as to allow fish to match on different - # criteria than prefix. - if test $nospace -ne 0; or test $nofiles -eq 0 - set prefix (commandline -t) - __%[1]s_debug "prefix: $prefix" - - set completions - for comp in $__%[1]s_comp_results - if test (string match -e -r "^$prefix" "$comp") - set -a completions $comp - end - end - set --global __%[1]s_comp_results $completions - __%[1]s_debug "Filtered completions are: $__%[1]s_comp_results" - - # Important not to quote the variable for count to work - set numComps (count $__%[1]s_comp_results) - __%[1]s_debug "numComps: $numComps" - - if test $numComps -eq 1; and test $nospace -ne 0 - # To support the "nospace" directive we trick the shell - # by outputting an extra, longer completion. - # We must first split on \t to get rid of the descriptions because - # the extra character we add to the fake second completion must be - # before the description. We don't need descriptions anyway since - # there is only a single real completion which the shell will expand - # immediately. - __%[1]s_debug "Adding second completion to perform nospace directive" - set split (string split --max 1 \t $__%[1]s_comp_results[1]) - set --global __%[1]s_comp_results $split[1] $split[1]. - __%[1]s_debug "Completions are now: $__%[1]s_comp_results" - end - - if test $numComps -eq 0; and test $nofiles -eq 0 - # To be consistent with bash and zsh, we only trigger file - # completion when there are no other completions - __%[1]s_debug "Requesting file completion" - return 1 - end + # Important not to quote the variable for count to work + set numComps (count $__%[1]s_comp_results) + __%[1]s_debug "numComps: $numComps" + + if test $numComps -eq 1; and test $nospace -ne 0 + # To support the "nospace" directive we trick the shell + # by outputting an extra, longer completion. + __%[1]s_debug "Adding second completion to perform nospace directive" + set --append __%[1]s_comp_results $__%[1]s_comp_results[1]. end - return 0 + if test $numComps -eq 0; and test $nofiles -eq 0 + __%[1]s_debug "Requesting file completion" + set --global __%[1]s_comp_do_file_comp 1 + end + + # If we don't want file completion, we must return true even if there + # are no completions found. This is because fish will perform the last + # completion command, even if its condition is false, if no other + # completion command was triggered + return (not set --query __%[1]s_comp_do_file_comp) end # Since Fish completions are only loaded once the user triggers them, we trigger them ourselves @@ -194,14 +170,21 @@ complete --do-complete "%[2]s " > /dev/null 2>&1 # Remove any pre-existing completions for the program since we will be handling all of them. complete -c %[2]s -e -# The call to __%[1]s_prepare_completions will setup __%[1]s_comp_results -# which provides the program's completion choices. +# The order in which the below two lines are defined is very important so that __%[1]s_prepare_completions +# is called first. It is __%[1]s_prepare_completions that sets up the __%[1]s_comp_do_file_comp variable. +# +# This completion will be run second as complete commands are added FILO. +# It triggers file completion choices when __%[1]s_comp_do_file_comp is set. +complete -c %[2]s -n 'set --query __%[1]s_comp_do_file_comp' + +# This completion will be run first as complete commands are added FILO. +# The call to __%[1]s_prepare_completions will setup both __%[1]s_comp_results and __%[1]s_comp_do_file_comp. +# It provides the program's completion choices. complete -c %[2]s -n '__%[1]s_prepare_completions' -f -a '$__%[1]s_comp_results' `, nameForVar, name, compCmd, ShellCompDirectiveError, ShellCompDirectiveNoSpace, ShellCompDirectiveNoFileComp, - ShellCompDirectiveFilterFileExt, ShellCompDirectiveFilterDirs, - shellCompDirectiveLegacyCustomComp, shellCompDirectiveLegacyCustomArgsComp)) + ShellCompDirectiveFilterFileExt, ShellCompDirectiveFilterDirs)) } // GenFishCompletion generates fish completion file and writes to the passed writer. diff --git a/vendor/github.com/spf13/cobra/zsh_completions.go b/vendor/github.com/spf13/cobra/zsh_completions.go index ef290e2eb..92a70394a 100644 --- a/vendor/github.com/spf13/cobra/zsh_completions.go +++ b/vendor/github.com/spf13/cobra/zsh_completions.go @@ -94,10 +94,8 @@ _%[1]s() local shellCompDirectiveNoFileComp=%[5]d local shellCompDirectiveFilterFileExt=%[6]d local shellCompDirectiveFilterDirs=%[7]d - local shellCompDirectiveLegacyCustomComp=%[8]d - local shellCompDirectiveLegacyCustomArgsComp=%[9]d - local lastParam lastChar flagPrefix requestComp out directive comp lastComp noSpace + local lastParam lastChar flagPrefix requestComp out directive compCount comp lastComp local -a completions __%[1]s_debug "\n========= starting completion logic ==========" @@ -165,6 +163,7 @@ _%[1]s() return fi + compCount=0 while IFS='\n' read -r comp; do if [ -n "$comp" ]; then # If requested, completions are returned with a description. @@ -176,17 +175,13 @@ _%[1]s() local tab=$(printf '\t') comp=${comp//$tab/:} + ((compCount++)) __%[1]s_debug "Adding completion: ${comp}" completions+=${comp} lastComp=$comp fi done < <(printf "%%s\n" "${out[@]}") - if [ $((directive & shellCompDirectiveNoSpace)) -ne 0 ]; then - __%[1]s_debug "Activating nospace." - noSpace="-S ''" - fi - if [ $((directive & shellCompDirectiveFilterFileExt)) -ne 0 ]; then # File extension filtering local filteringCmd @@ -213,40 +208,25 @@ _%[1]s() __%[1]s_debug "Listing directories in ." fi - local result _arguments '*:dirname:_files -/'" ${flagPrefix}" - result=$? if [ -n "$subdir" ]; then popd >/dev/null 2>&1 fi - return $result - else - __%[1]s_debug "Calling _describe" - if eval _describe "completions" completions $flagPrefix $noSpace; then - __%[1]s_debug "_describe found some completions" - - # Return the success of having called _describe - return 0 + elif [ $((directive & shellCompDirectiveNoSpace)) -ne 0 ] && [ ${compCount} -eq 1 ]; then + __%[1]s_debug "Activating nospace." + # We can use compadd here as there is no description when + # there is only one completion. + compadd -S '' "${lastComp}" + elif [ ${compCount} -eq 0 ]; then + if [ $((directive & shellCompDirectiveNoFileComp)) -ne 0 ]; then + __%[1]s_debug "deactivating file completion" else - __%[1]s_debug "_describe did not find completions." - __%[1]s_debug "Checking if we should do file completion." - if [ $((directive & shellCompDirectiveNoFileComp)) -ne 0 ]; then - __%[1]s_debug "deactivating file completion" - - # We must return an error code here to let zsh know that there were no - # completions found by _describe; this is what will trigger other - # matching algorithms to attempt to find completions. - # For example zsh can match letters in the middle of words. - return 1 - else - # Perform file completion - __%[1]s_debug "Activating file completion" - - # We must return the result of this command, so it must be the - # last command, or else we must store its result to return it. - _arguments '*:filename:_files'" ${flagPrefix}" - fi + # Perform file completion + __%[1]s_debug "activating file completion" + _arguments '*:filename:_files'" ${flagPrefix}" fi + else + _describe "completions" completions $(echo $flagPrefix) fi } @@ -256,6 +236,5 @@ if [ "$funcstack[1]" = "_%[1]s" ]; then fi `, name, compCmd, ShellCompDirectiveError, ShellCompDirectiveNoSpace, ShellCompDirectiveNoFileComp, - ShellCompDirectiveFilterFileExt, ShellCompDirectiveFilterDirs, - shellCompDirectiveLegacyCustomComp, shellCompDirectiveLegacyCustomArgsComp)) + ShellCompDirectiveFilterFileExt, ShellCompDirectiveFilterDirs)) } diff --git a/vendor/modules.txt b/vendor/modules.txt index 857e84cae..9de30e604 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -517,7 +517,7 @@ github.com/seccomp/libseccomp-golang # github.com/sirupsen/logrus v1.7.0 github.com/sirupsen/logrus github.com/sirupsen/logrus/hooks/syslog -# github.com/spf13/cobra v1.1.1 => github.com/Luap99/cobra v1.0.1-0.20201110155035-83a59186c706 +# github.com/spf13/cobra v1.1.1 github.com/spf13/cobra # github.com/spf13/pflag v1.0.5 github.com/spf13/pflag |