diff options
96 files changed, 3993 insertions, 863 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/completion.go b/cmd/podman/common/completion.go index 9856e46ef..7fed15e5e 100644 --- a/cmd/podman/common/completion.go +++ b/cmd/podman/common/completion.go @@ -861,10 +861,10 @@ func AutocompletePsFilters(cmd *cobra.Command, args []string, toComplete string) "status=": func(_ string) ([]string, cobra.ShellCompDirective) { return containerStatuses, cobra.ShellCompDirectiveNoFileComp }, - "ancestor": func(s string) ([]string, cobra.ShellCompDirective) { return getImages(cmd, s) }, - "before=": func(s string) ([]string, cobra.ShellCompDirective) { return getContainers(cmd, s, completeDefault) }, - "since=": func(s string) ([]string, cobra.ShellCompDirective) { return getContainers(cmd, s, completeDefault) }, - "volume=": func(s string) ([]string, cobra.ShellCompDirective) { return getVolumes(cmd, s) }, + "ancestor=": func(s string) ([]string, cobra.ShellCompDirective) { return getImages(cmd, s) }, + "before=": func(s string) ([]string, cobra.ShellCompDirective) { return getContainers(cmd, s, completeDefault) }, + "since=": func(s string) ([]string, cobra.ShellCompDirective) { return getContainers(cmd, s, completeDefault) }, + "volume=": func(s string) ([]string, cobra.ShellCompDirective) { return getVolumes(cmd, s) }, "health=": func(_ string) ([]string, cobra.ShellCompDirective) { return []string{define.HealthCheckHealthy, define.HealthCheckUnhealthy}, cobra.ShellCompDirectiveNoFileComp 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..4b0e40df2 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 @@ -203,20 +204,14 @@ func ContainerCreateToContainerCLIOpts(cc handlers.CreateContainerConfig, cgroup for _, m := range cc.HostConfig.Mounts { mount := fmt.Sprintf("type=%s", m.Type) if len(m.Source) > 0 { - mount += fmt.Sprintf("source=%s", m.Source) + mount += fmt.Sprintf(",source=%s", m.Source) } if len(m.Target) > 0 { - mount += fmt.Sprintf("dest=%s", m.Target) + mount += fmt.Sprintf(",dst=%s", m.Target) } 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/list.go b/cmd/podman/images/list.go index bcb31e6ee..8a7951923 100644 --- a/cmd/podman/images/list.go +++ b/cmd/podman/images/list.go @@ -126,8 +126,8 @@ func images(cmd *cobra.Command, args []string) error { case listFlag.quiet: return writeID(imgs) default: - if cmd.Flag("format").Changed { - listFlag.noHeading = true // V1 compatibility + if cmd.Flags().Changed("format") && !parse.HasTable(listFlag.format) { + listFlag.noHeading = true } return writeTemplate(imgs) } 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..8db4bb89a 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,8 @@ var ( var ( networkCreateOptions entities.NetworkCreateOptions + labels []string + opts []string ) func networkCreateFlags(cmd *cobra.Command) { @@ -36,6 +40,10 @@ func networkCreateFlags(cmd *cobra.Command) { flags.StringVarP(&networkCreateOptions.Driver, driverFlagName, "d", "bridge", "driver to manage the network") _ = cmd.RegisterFlagCompletionFunc(driverFlagName, common.AutocompleteNetworkDriver) + optFlagName := "opt" + flags.StringArrayVarP(&opts, optFlagName, "o", []string{}, "Set driver specific options (default [])") + _ = cmd.RegisterFlagCompletionFunc(optFlagName, completion.AutocompleteNone) + gatewayFlagName := "gateway" flags.IPVar(&networkCreateOptions.Gateway, gatewayFlagName, nil, "IPv4 or IPv6 gateway for the subnet") _ = cmd.RegisterFlagCompletionFunc(gatewayFlagName, completion.AutocompleteNone) @@ -50,6 +58,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 +93,15 @@ 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") + } + networkCreateOptions.Options, err = parse.GetAllLabels([]string{}, opts) + if err != nil { + return errors.Wrapf(err, "unable to process options") + } 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..16ae980dc 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,20 @@ var ( var ( networkListOptions entities.NetworkListOptions + filters []string + noTrunc bool ) 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") + flags.BoolVar(&noTrunc, "no-trunc", false, "Do not truncate the network ID") 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 +65,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 +97,8 @@ func networkList(cmd *cobra.Command, args []string) error { "CNIVersion": "version", "Version": "version", "Plugins": "plugins", + "Labels": "labels", + "ID": "network id", }) renderHeaders := true row := "{{.Name}}\t{{.Version}}\t{{.Plugins}}\n" @@ -144,3 +150,19 @@ 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, ",") +} + +func (n ListPrintReports) ID() string { + length := 12 + if noTrunc { + length = 64 + } + return network.GetNetworkID(n.Name)[:length] +} 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-load.1.md b/docs/source/markdown/podman-load.1.md index 177709a43..dc2a632e5 100644 --- a/docs/source/markdown/podman-load.1.md +++ b/docs/source/markdown/podman-load.1.md @@ -10,7 +10,7 @@ podman\-load - Load image(s) from a tar archive into container storage ## DESCRIPTION **podman load** loads an image from either an **oci-archive** or a **docker-archive** stored on the local machine into container storage. **podman load** reads from stdin by default or a file if the **input** option is set. -You can also specify a name for the image if the archive does not contain a named reference, of if you want an additional name for the local image. +You can also specify a name for the image if the archive is of single image and load will tag an additional image with the name:tag. **podman load** is used for loading from the archive generated by **podman save**, that includes the image parent layers. To load the archive of container's filesystem created by **podman export**, use **podman import**. The local client further supports loading an **oci-dir** or a **docker-dir** as created with **podman save** (1). diff --git a/docs/source/markdown/podman-network-create.1.md b/docs/source/markdown/podman-network-create.1.md index cbf9d26dc..16e4e3bdb 100644 --- a/docs/source/markdown/podman-network-create.1.md +++ b/docs/source/markdown/podman-network-create.1.md @@ -26,6 +26,14 @@ resolution. Driver to manage the network (default "bridge"). Currently only `bridge` is supported. +#### **--opt**=*option*, **-o** + +Set driver specific options. + +For the `bridge` driver the following options are supported: `mtu` and `vlan`. +The `mtu` option sets the Maximum Transmission Unit (MTU) and takes an integer value. +The `vlan` option assign VLAN tag and enables vlan\_filtering. Defaults to none. + #### **--gateway** Define a gateway for the subnet. If you want to provide a gateway address, you must also provide a @@ -40,6 +48,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..a964c97e8 100644 --- a/docs/source/markdown/podman-network-ls.1.md +++ b/docs/source/markdown/podman-network-ls.1.md @@ -10,17 +10,44 @@ podman\-network\-ls - Display a summary of CNI networks Displays a list of existing podman networks. This command is not available for rootless users. ## OPTIONS -#### **--quiet**, **-q** +#### **--filter**, **-f** -The `quiet` option will restrict the output to only the network names. +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) | +| id | [ID] Full or partial network ID | +| 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 | + +#### **--format** -#### **--format**, **-f** +Change the default output format. This can be of a supported type like 'json' +or a Go template. +Valid placeholders for the Go template are listed below: -Pretty-print networks to JSON or using a Go template. +| **Placeholder** | **Description** | +| --------------- | --------------------------------| +| .ID | Network ID | +| .Name | Network name | +| .Plugins | Network Plugins | +| .Labels | Network labels | +| .Version | CNI Version of the config file | -#### **--filter** +#### **--no-trunc** -Provide filter values (e.g. 'name=podman'). +Do not truncate the network ID. The network ID is not displayed by default and must be specified with **--format**. + +#### **--quiet**, **-q** + +The `quiet` option will restrict the output to only the network names. ## 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 11840511c..1038906c0 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:**. @@ -23,6 +23,7 @@ require ( github.com/docker/distribution v2.7.1+incompatible github.com/docker/docker v17.12.0-ce-rc1.0.20201020191947-73dc6a680cdd+incompatible github.com/docker/go-connections v0.4.0 + github.com/docker/go-plugins-helpers v0.0.0-20200102110956-c9a8a2d92ccc github.com/docker/go-units v0.4.0 github.com/fsnotify/fsnotify v1.4.9 github.com/ghodss/yaml v1.0.0 @@ -74,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= @@ -152,6 +150,8 @@ github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKoh github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= github.com/docker/go-metrics v0.0.1 h1:AgB/0SvBxihN0X8OR4SjsblXkbMvalQ8cjmtKQ2rQV8= github.com/docker/go-metrics v0.0.1/go.mod h1:cG1hvH2utMXtqgqqYE9plW6lDxS3/5ayHzueweSI3Vw= +github.com/docker/go-plugins-helpers v0.0.0-20200102110956-c9a8a2d92ccc h1:/A+mPcpajLsWiX9gSnzdVKM/IzZoYiNqXHe83z50k2c= +github.com/docker/go-plugins-helpers v0.0.0-20200102110956-c9a8a2d92ccc/go.mod h1:LFyLie6XcDbyKGeVK6bHe+9aJTYCxWLBg5IrJZOaXKA= github.com/docker/go-units v0.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw= github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/docker/libnetwork v0.8.0-dev.2.0.20190625141545-5a177b73e316 h1:moehPjPiGUaWdwgOl92xRyFHJyaqXDHcCyW9M6nmCK4= @@ -500,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..094fbe349 100644 --- a/libpod/network/create.go +++ b/libpod/network/create.go @@ -6,6 +6,7 @@ import ( "io/ioutil" "os" "path/filepath" + "strconv" "github.com/containernetworking/cni/pkg/version" "github.com/containers/common/pkg/config" @@ -76,6 +77,29 @@ func validateBridgeOptions(options entities.NetworkCreateOptions) error { } +// parseMTU parses the mtu option +func parseMTU(mtu string) (int, error) { + if mtu == "" { + return 0, nil // default + } + m, err := strconv.Atoi(mtu) + if err != nil { + return 0, err + } + if m < 0 { + return 0, errors.Errorf("the value %d for mtu is less than zero", m) + } + return m, nil +} + +// parseVlan parses the vlan option +func parseVlan(vlan string) (int, error) { + if vlan == "" { + return 0, nil // default + } + return strconv.Atoi(vlan) +} + // createBridge creates a CNI network func createBridge(name string, options entities.NetworkCreateOptions, runtimeConfig *config.Config) (string, error) { var ( @@ -149,6 +173,28 @@ func createBridge(name string, options entities.NetworkCreateOptions, runtimeCon ipMasq = false } + var mtu int + var vlan int + for k, v := range options.Options { + var err error + switch k { + case "mtu": + mtu, err = parseMTU(v) + if err != nil { + return "", err + } + + case "vlan": + vlan, err = parseVlan(v) + if err != nil { + return "", err + } + + default: + return "", errors.Errorf("unsupported option %s", k) + } + } + // obtain host bridge name bridgeDeviceName, err := GetFreeDeviceName(runtimeConfig) if err != nil { @@ -169,10 +215,10 @@ 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) + bridge := NewHostLocalBridge(bridgeDeviceName, isGateway, false, ipMasq, mtu, vlan, ipamConfig) plugins = append(plugins, bridge) plugins = append(plugins, NewPortMapPlugin()) plugins = append(plugins, NewFirewallPlugin()) @@ -223,7 +269,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..33cf01064 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 @@ -49,13 +50,15 @@ func LoadCNIConfsFromDir(dir string) ([]*libcni.NetworkConfigList, error) { return configs, nil } -// GetCNIConfigPathByName finds a CNI network by name and +// GetCNIConfigPathByNameOrID finds a CNI network by name and // returns its configuration file path -func GetCNIConfigPathByName(config *config.Config, name string) (string, error) { +func GetCNIConfigPathByNameOrID(config *config.Config, name string) (string, error) { files, err := libcni.ConfFiles(GetCNIConfDir(config), []string{".conflist"}) if err != nil { return "", err } + idMatch := 0 + file := "" for _, confFile := range files { conf, err := libcni.ConfListFromFile(confFile) if err != nil { @@ -64,6 +67,16 @@ func GetCNIConfigPathByName(config *config.Config, name string) (string, error) if conf.Name == name { return confFile, nil } + if strings.HasPrefix(GetNetworkID(conf.Name), name) { + idMatch++ + file = confFile + } + } + if idMatch == 1 { + return file, nil + } + if idMatch > 1 { + return "", errors.Errorf("more than one result for network ID %s", name) } return "", errors.Wrap(define.ErrNoSuchNetwork, fmt.Sprintf("unable to find network configuration for %s", name)) } @@ -71,7 +84,7 @@ func GetCNIConfigPathByName(config *config.Config, name string) (string, error) // ReadRawCNIConfByName reads the raw CNI configuration for a CNI // network by name func ReadRawCNIConfByName(config *config.Config, name string) ([]byte, error) { - confFile, err := GetCNIConfigPathByName(config, name) + confFile, err := GetCNIConfigPathByNameOrID(config, name) if err != nil { return nil, err } @@ -89,6 +102,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..d61b96ecb 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,22 +19,36 @@ 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 } // NewHostLocalBridge creates a new LocalBridge for host-local -func NewHostLocalBridge(name string, isGateWay, isDefaultGW, ipMasq bool, ipamConf IPAMHostLocalConf) *HostLocalBridge { +func NewHostLocalBridge(name string, isGateWay, isDefaultGW, ipMasq bool, mtu int, vlan int, ipamConf IPAMHostLocalConf) *HostLocalBridge { hostLocalBridge := HostLocalBridge{ PluginType: "bridge", BrName: name, IPMasq: ipMasq, + MTU: mtu, HairpinMode: true, + Vlan: vlan, IPAM: ipamConf, } if isGateWay { @@ -159,3 +178,72 @@ 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 + } + } + + case "id": + // matches part of one id + for _, filterValue := range filterValues { + if strings.Contains(GetNetworkID(netconf.Name), filterValue) { + result = true + break + } + } + + // TODO: add dangling filter + + default: + return false, errors.Errorf("invalid filter %q", key) + } + } + return result, nil +} diff --git a/libpod/network/network.go b/libpod/network/network.go index 0febb52f6..89f0b67ac 100644 --- a/libpod/network/network.go +++ b/libpod/network/network.go @@ -1,6 +1,8 @@ package network import ( + "crypto/sha256" + "encoding/hex" "encoding/json" "net" "os" @@ -175,7 +177,7 @@ func RemoveNetwork(config *config.Config, name string) error { return err } defer l.releaseCNILock() - cniPath, err := GetCNIConfigPathByName(config, name) + cniPath, err := GetCNIConfigPathByNameOrID(config, name) if err != nil { return err } @@ -229,3 +231,10 @@ func Exists(config *config.Config, name string) (bool, error) { } return true, nil } + +// GetNetworkID return the network ID for a given name. +// It is just the sha256 hash but this should be good enough. +func GetNetworkID(name string) string { + hash := sha256.Sum256([]byte(name)) + return hex.EncodeToString(hash[:]) +} diff --git a/libpod/networking_linux.go b/libpod/networking_linux.go index 4e7ffaf81..15e470c80 100644 --- a/libpod/networking_linux.go +++ b/libpod/networking_linux.go @@ -1047,21 +1047,25 @@ func (c *Container) NetworkDisconnect(nameOrID, netName string, force bool) erro return err } + if err := c.runtime.state.NetworkDisconnect(c, netName); err != nil { + return err + } + + c.newNetworkEvent(events.NetworkDisconnect, netName) if c.state.State != define.ContainerStateRunning { - return errors.Wrapf(define.ErrCtrStateInvalid, "cannot disconnect container %s from networks as it is not running", nameOrID) + return nil } + if c.state.NetNS == nil { return errors.Wrapf(define.ErrNoNetwork, "unable to disconnect %s from %s", nameOrID, netName) } + podConfig := c.runtime.getPodNetwork(c.ID(), c.Name(), c.state.NetNS.Path(), []string{netName}, c.config.PortMappings, nil, nil, c.state.NetInterfaceDescriptions) if err := c.runtime.netPlugin.TearDownPod(podConfig); err != nil { return err } - if err := c.runtime.state.NetworkDisconnect(c, netName); err != nil { - return err - } - // update network status + // update network status if container is not running networkStatus := c.state.NetworkStatus // clip out the index of the network tmpNetworkStatus := make([]*cnitypes.Result, len(networkStatus)-1) @@ -1071,7 +1075,6 @@ func (c *Container) NetworkDisconnect(nameOrID, netName string, force bool) erro } } c.state.NetworkStatus = tmpNetworkStatus - c.newNetworkEvent(events.NetworkDisconnect, netName) return c.save() } @@ -1096,15 +1099,16 @@ func (c *Container) NetworkConnect(nameOrID, netName string, aliases []string) e return err } + if err := c.runtime.state.NetworkConnect(c, netName, aliases); err != nil { + return err + } + c.newNetworkEvent(events.NetworkConnect, netName) if c.state.State != define.ContainerStateRunning { - return errors.Wrapf(define.ErrCtrStateInvalid, "cannot connect container %s to networks as it is not running", nameOrID) + return nil } if c.state.NetNS == nil { return errors.Wrapf(define.ErrNoNetwork, "unable to connect %s to %s", nameOrID, netName) } - if err := c.runtime.state.NetworkConnect(c, netName, aliases); err != nil { - return err - } ctrNetworks, _, err := c.networks() if err != nil { @@ -1159,7 +1163,6 @@ func (c *Container) NetworkConnect(nameOrID, netName string, aliases []string) e networkStatus[index] = networkResults[0] c.state.NetworkStatus = networkStatus } - c.newNetworkEvent(events.NetworkConnect, netName) return c.save() } diff --git a/libpod/options.go b/libpod/options.go index 0f55f34a3..bd12c0c34 100644 --- a/libpod/options.go +++ b/libpod/options.go @@ -1547,12 +1547,19 @@ func WithVolumeDriver(driver string) VolumeCreateOption { if volume.valid { return define.ErrVolumeFinalized } - // only local driver is possible rn + + // Uncomment when volume plugins are ready for use. + // if driver != define.VolumeDriverLocal { + // if _, err := plugin.GetVolumePlugin(driver); err != nil { + // return err + // } + // } + if driver != define.VolumeDriverLocal { return define.ErrNotImplemented - } - volume.config.Driver = define.VolumeDriverLocal + + volume.config.Driver = driver return nil } } diff --git a/libpod/plugin/volume_api.go b/libpod/plugin/volume_api.go new file mode 100644 index 000000000..2500a4f36 --- /dev/null +++ b/libpod/plugin/volume_api.go @@ -0,0 +1,454 @@ +package plugin + +import ( + "bytes" + "fmt" + "io/ioutil" + "net/http" + "os" + "path/filepath" + "strings" + "sync" + "time" + + "github.com/containers/podman/v2/libpod/define" + "github.com/docker/go-plugins-helpers/sdk" + "github.com/docker/go-plugins-helpers/volume" + jsoniter "github.com/json-iterator/go" + "github.com/pkg/errors" + "github.com/sirupsen/logrus" +) + +var json = jsoniter.ConfigCompatibleWithStandardLibrary + +// TODO: We should add syntax for specifying plugins to containers.conf, and +// support for loading based on that. + +// Copied from docker/go-plugins-helpers/volume/api.go - not exported, so we +// need to do this to get at them. +// These are well-established paths that should not change unless the plugin API +// version changes. +var ( + activatePath = "/Plugin.Activate" + createPath = "/VolumeDriver.Create" + getPath = "/VolumeDriver.Get" + listPath = "/VolumeDriver.List" + removePath = "/VolumeDriver.Remove" + hostVirtualPath = "/VolumeDriver.Path" + mountPath = "/VolumeDriver.Mount" + unmountPath = "/VolumeDriver.Unmount" + // nolint + capabilitiesPath = "/VolumeDriver.Capabilities" +) + +const ( + defaultTimeout = 5 * time.Second + defaultPath = "/run/docker/plugins" + volumePluginType = "VolumeDriver" +) + +var ( + ErrNotPlugin = errors.New("target does not appear to be a valid plugin") + ErrNotVolumePlugin = errors.New("plugin is not a volume plugin") + ErrPluginRemoved = errors.New("plugin is no longer available (shut down?)") + + // This stores available, initialized volume plugins. + pluginsLock sync.Mutex + plugins map[string]*VolumePlugin +) + +// VolumePlugin is a single volume plugin. +type VolumePlugin struct { + // Name is the name of the volume plugin. This will be used to refer to + // it. + Name string + // SocketPath is the unix socket at which the plugin is accessed. + SocketPath string +} + +// This is the response from the activate endpoint of the API. +type activateResponse struct { + Implements []string +} + +// Validate that the given plugin is good to use. +// Add it to available plugins if so. +func validatePlugin(newPlugin *VolumePlugin) error { + // It's a socket. Is it a plugin? + // Hit the Activate endpoint to find out if it is, and if so what kind + req, err := http.NewRequest("POST", activatePath, nil) + if err != nil { + return errors.Wrapf(err, "error making request to volume plugin %s activation endpoint", newPlugin.Name) + } + + req.Header.Set("Host", newPlugin.getURI()) + req.Header.Set("Content-Type", sdk.DefaultContentTypeV1_1) + + client := new(http.Client) + client.Timeout = defaultTimeout + resp, err := client.Do(req) + if err != nil { + return errors.Wrapf(err, "error sending request to plugin %s activation endpoint", newPlugin.Name) + } + defer resp.Body.Close() + + // Response code MUST be 200. Anything else, we have to assume it's not + // a valid plugin. + if resp.StatusCode != 200 { + return errors.Wrapf(ErrNotPlugin, "got status code %d from activation endpoint for plugin %s", resp.StatusCode, newPlugin.Name) + } + + // Read and decode the body so we can tell if this is a volume plugin. + respBytes, err := ioutil.ReadAll(resp.Body) + if err != nil { + return errors.Wrapf(err, "error reading activation response body from plugin %s", newPlugin.Name) + } + + respStruct := new(activateResponse) + if err := json.Unmarshal(respBytes, respStruct); err != nil { + return errors.Wrapf(err, "error unmarshalling plugin %s activation response", newPlugin.Name) + } + + foundVolume := false + for _, pluginType := range respStruct.Implements { + if pluginType == volumePluginType { + foundVolume = true + break + } + } + + if !foundVolume { + return errors.Wrapf(ErrNotVolumePlugin, "plugin %s does not implement volume plugin, instead provides %s", newPlugin.Name, strings.Join(respStruct.Implements, ", ")) + } + + plugins[newPlugin.Name] = newPlugin + + return nil +} + +// GetVolumePlugin gets a single volume plugin by path. +// TODO: We should not be auto-completing based on a default path; we should +// require volumes to have been pre-specified in containers.conf (will need a +// function to pre-populate the plugins list, and we should probably do a lazy +// initialization there to not slow things down too much). +func GetVolumePlugin(name string) (*VolumePlugin, error) { + pluginsLock.Lock() + defer pluginsLock.Unlock() + + plugin, exists := plugins[name] + if exists { + return plugin, nil + } + + // It's not cached. We need to get it. + + newPlugin := new(VolumePlugin) + newPlugin.Name = name + newPlugin.SocketPath = filepath.Join(defaultPath, fmt.Sprintf("%s.sock", name)) + + stat, err := os.Stat(newPlugin.SocketPath) + if err != nil { + return nil, errors.Wrapf(err, "cannot access plugin %s socket %q", name, newPlugin.SocketPath) + } + if stat.Mode()&os.ModeSocket == 0 { + return nil, errors.Wrapf(ErrNotPlugin, "volume %s path %q is not a unix socket", name, newPlugin.SocketPath) + } + + if err := validatePlugin(newPlugin); err != nil { + return nil, err + } + + return newPlugin, nil +} + +func (p *VolumePlugin) getURI() string { + return "unix://" + p.SocketPath +} + +// Verify the plugin is still available. +// TODO: Do we want to ping with an HTTP request? There's no ping endpoint so +// we'd need to hit Activate or Capabilities? +func (p *VolumePlugin) verifyReachable() error { + if _, err := os.Stat(p.SocketPath); err != nil { + if os.IsNotExist(err) { + pluginsLock.Lock() + defer pluginsLock.Unlock() + delete(plugins, p.Name) + return errors.Wrapf(ErrPluginRemoved, p.Name) + } + + return errors.Wrapf(err, "error accessing plugin %s", p.Name) + } + return nil +} + +// Send a request to the volume plugin for handling. +func (p *VolumePlugin) sendRequest(toJSON interface{}, hasBody bool, endpoint string) (*http.Response, error) { + var ( + reqJSON []byte + err error + ) + + if hasBody { + reqJSON, err = json.Marshal(toJSON) + if err != nil { + return nil, errors.Wrapf(err, "error marshalling request JSON for volume plugin %s endpoint %s", p.Name, endpoint) + } + } + + req, err := http.NewRequest("POST", endpoint, bytes.NewReader(reqJSON)) + if err != nil { + return nil, errors.Wrapf(err, "error making request to volume plugin %s endpoint %s", p.Name, endpoint) + } + + req.Header.Set("Host", p.getURI()) + req.Header.Set("Content-Type", sdk.DefaultContentTypeV1_1) + + client := new(http.Client) + client.Timeout = defaultTimeout + resp, err := client.Do(req) + if err != nil { + return nil, errors.Wrapf(err, "error sending request to volume plugin %s endpoint %s", p.Name, endpoint) + } + defer resp.Body.Close() + + return resp, nil +} + +// Turn an error response from a volume plugin into a well-formatted Go error. +func (p *VolumePlugin) makeErrorResponse(err, endpoint, volName string) error { + if err == "" { + err = "empty error from plugin" + } + if volName != "" { + return errors.Wrapf(errors.New(err), "error on %s on volume %s in volume plugin %s", endpoint, volName, p.Name) + } else { + return errors.Wrapf(errors.New(err), "error on %s in volume plugin %s", endpoint, p.Name) + } +} + +// Handle error responses from plugin +func (p *VolumePlugin) handleErrorResponse(resp *http.Response, endpoint, volName string) error { + // The official plugin reference implementation uses HTTP 500 for + // errors, but I don't think we can guarantee all plugins do that. + // Let's interpret anything other than 200 as an error. + // If there isn't an error, don't even bother decoding the response. + if resp.StatusCode != 200 { + errResp, err := ioutil.ReadAll(resp.Body) + if err != nil { + return errors.Wrapf(err, "error reading response body from volume plugin %s", p.Name) + } + + errStruct := new(volume.ErrorResponse) + if err := json.Unmarshal(errResp, errStruct); err != nil { + return errors.Wrapf(err, "error unmarshalling JSON response from volume plugin %s", p.Name) + } + + return p.makeErrorResponse(errStruct.Err, endpoint, volName) + } + + return nil +} + +// CreateVolume creates a volume in the plugin. +func (p *VolumePlugin) CreateVolume(req *volume.CreateRequest) error { + if req == nil { + return errors.Wrapf(define.ErrInvalidArg, "must provide non-nil request to CreateVolume") + } + + if err := p.verifyReachable(); err != nil { + return err + } + + logrus.Infof("Creating volume %s using plugin %s", req.Name, p.Name) + + resp, err := p.sendRequest(req, true, createPath) + if err != nil { + return err + } + defer resp.Body.Close() + + return p.handleErrorResponse(resp, createPath, req.Name) +} + +// ListVolumes lists volumes available in the plugin. +func (p *VolumePlugin) ListVolumes() ([]*volume.Volume, error) { + if err := p.verifyReachable(); err != nil { + return nil, err + } + + logrus.Infof("Listing volumes using plugin %s", p.Name) + + resp, err := p.sendRequest(nil, false, listPath) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + if err := p.handleErrorResponse(resp, listPath, ""); err != nil { + return nil, err + } + + // TODO: Can probably unify response reading under a helper + volumeRespBytes, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, errors.Wrapf(err, "error reading response body from volume plugin %s", p.Name) + } + + volumeResp := new(volume.ListResponse) + if err := json.Unmarshal(volumeRespBytes, volumeResp); err != nil { + return nil, errors.Wrapf(err, "error unmarshalling volume plugin %s list response", p.Name) + } + + return volumeResp.Volumes, nil +} + +// GetVolume gets a single volume from the plugin. +func (p *VolumePlugin) GetVolume(req *volume.GetRequest) (*volume.Volume, error) { + if req == nil { + return nil, errors.Wrapf(define.ErrInvalidArg, "must provide non-nil request to GetVolume") + } + + if err := p.verifyReachable(); err != nil { + return nil, err + } + + logrus.Infof("Getting volume %s using plugin %s", req.Name, p.Name) + + resp, err := p.sendRequest(req, true, getPath) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + if err := p.handleErrorResponse(resp, getPath, req.Name); err != nil { + return nil, err + } + + getRespBytes, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, errors.Wrapf(err, "error reading response body from volume plugin %s", p.Name) + } + + getResp := new(volume.GetResponse) + if err := json.Unmarshal(getRespBytes, getResp); err != nil { + return nil, errors.Wrapf(err, "error unmarshalling volume plugin %s get response", p.Name) + } + + return getResp.Volume, nil +} + +// RemoveVolume removes a single volume from the plugin. +func (p *VolumePlugin) RemoveVolume(req *volume.RemoveRequest) error { + if req == nil { + return errors.Wrapf(define.ErrInvalidArg, "must provide non-nil request to RemoveVolume") + } + + if err := p.verifyReachable(); err != nil { + return err + } + + logrus.Infof("Removing volume %s using plugin %s", req.Name, p.Name) + + resp, err := p.sendRequest(req, true, removePath) + if err != nil { + return err + } + defer resp.Body.Close() + + return p.handleErrorResponse(resp, removePath, req.Name) +} + +// GetVolumePath gets the path the given volume is mounted at. +func (p *VolumePlugin) GetVolumePath(req *volume.PathRequest) (string, error) { + if req == nil { + return "", errors.Wrapf(define.ErrInvalidArg, "must provide non-nil request to GetVolumePath") + } + + if err := p.verifyReachable(); err != nil { + return "", err + } + + logrus.Infof("Getting volume %s path using plugin %s", req.Name, p.Name) + + resp, err := p.sendRequest(req, true, hostVirtualPath) + if err != nil { + return "", err + } + defer resp.Body.Close() + + if err := p.handleErrorResponse(resp, hostVirtualPath, req.Name); err != nil { + return "", err + } + + pathRespBytes, err := ioutil.ReadAll(resp.Body) + if err != nil { + return "", errors.Wrapf(err, "error reading response body from volume plugin %s", p.Name) + } + + pathResp := new(volume.PathResponse) + if err := json.Unmarshal(pathRespBytes, pathResp); err != nil { + return "", errors.Wrapf(err, "error unmarshalling volume plugin %s path response", p.Name) + } + + return pathResp.Mountpoint, nil +} + +// MountVolume mounts the given volume. The ID argument is the ID of the +// mounting container, used for internal record-keeping by the plugin. Returns +// the path the volume has been mounted at. +func (p *VolumePlugin) MountVolume(req *volume.MountRequest) (string, error) { + if req == nil { + return "", errors.Wrapf(define.ErrInvalidArg, "must provide non-nil request to MountVolume") + } + + if err := p.verifyReachable(); err != nil { + return "", err + } + + logrus.Infof("Mounting volume %s using plugin %s for container %s", req.Name, p.Name, req.ID) + + resp, err := p.sendRequest(req, true, mountPath) + if err != nil { + return "", err + } + defer resp.Body.Close() + + if err := p.handleErrorResponse(resp, mountPath, req.Name); err != nil { + return "", err + } + + mountRespBytes, err := ioutil.ReadAll(resp.Body) + if err != nil { + return "", errors.Wrapf(err, "error reading response body from volume plugin %s", p.Name) + } + + mountResp := new(volume.MountResponse) + if err := json.Unmarshal(mountRespBytes, mountResp); err != nil { + return "", errors.Wrapf(err, "error unmarshalling volume plugin %s path response", p.Name) + } + + return mountResp.Mountpoint, nil +} + +// UnmountVolume unmounts the given volume. The ID argument is the ID of the +// container that is unmounting, used for internal record-keeping by the plugin. +func (p *VolumePlugin) UnmountVolume(req *volume.UnmountRequest) error { + if req == nil { + return errors.Wrapf(define.ErrInvalidArg, "must provide non-nil request to UnmountVolume") + } + + if err := p.verifyReachable(); err != nil { + return err + } + + logrus.Infof("Unmounting volume %s using plugin %s for container %s", req.Name, p.Name, req.ID) + + resp, err := p.sendRequest(req, true, unmountPath) + if err != nil { + return err + } + defer resp.Body.Close() + + return p.handleErrorResponse(resp, unmountPath, req.Name) +} diff --git a/libpod/reset.go b/libpod/reset.go index f8828fed4..6d2842723 100644 --- a/libpod/reset.go +++ b/libpod/reset.go @@ -46,7 +46,7 @@ func (r *Runtime) Reset(ctx context.Context) error { } } - if err := stopPauseProcess(); err != nil { + if err := r.stopPauseProcess(); err != nil { logrus.Errorf("Error stopping pause process: %v", err) } diff --git a/libpod/runtime.go b/libpod/runtime.go index df3dfae2b..cdf66a4d0 100644 --- a/libpod/runtime.go +++ b/libpod/runtime.go @@ -472,7 +472,7 @@ func makeRuntime(ctx context.Context, runtime *Runtime) (retErr error) { // we will need to access the storage. if os.Geteuid() != 0 { aliveLock.Unlock() // Unlock to avoid deadlock as BecomeRootInUserNS will reexec. - pausePid, err := util.GetRootlessPauseProcessPidPath() + pausePid, err := util.GetRootlessPauseProcessPidPathGivenDir(runtime.config.Engine.TmpDir) if err != nil { return errors.Wrapf(err, "could not get pause process pid file path") } @@ -538,6 +538,15 @@ func makeRuntime(ctx context.Context, runtime *Runtime) (retErr error) { return nil } +// TmpDir gets the current Libpod temporary files directory. +func (r *Runtime) TmpDir() (string, error) { + if !r.valid { + return "", define.ErrRuntimeStopped + } + + return r.config.Engine.TmpDir, nil +} + // GetConfig returns a copy of the configuration used by the runtime func (r *Runtime) GetConfig() (*config.Config, error) { r.lock.RLock() diff --git a/libpod/runtime_img.go b/libpod/runtime_img.go index e57890fa2..a2d9a875e 100644 --- a/libpod/runtime_img.go +++ b/libpod/runtime_img.go @@ -8,7 +8,6 @@ import ( "net/http" "net/url" "os" - "strings" "github.com/containers/buildah/imagebuildah" "github.com/containers/image/v5/directory" @@ -276,56 +275,47 @@ func DownloadFromFile(reader *os.File) (string, error) { } // LoadImage loads a container image into local storage -func (r *Runtime) LoadImage(ctx context.Context, name, inputFile string, writer io.Writer, signaturePolicy string) (string, error) { - var ( - newImages []*image.Image - err error - src types.ImageReference - ) +func (r *Runtime) LoadImage(ctx context.Context, inputFile string, writer io.Writer, signaturePolicy string) (string, error) { + if newImages, err := r.LoadAllImageFromArchive(ctx, writer, inputFile, signaturePolicy); err == nil { + return newImages, nil + } + return r.LoadImageFromSingleImageArchive(ctx, writer, inputFile, signaturePolicy) +} - if name == "" { - newImages, err = r.ImageRuntime().LoadAllImagesFromDockerArchive(ctx, inputFile, signaturePolicy, writer) - if err == nil { - return getImageNames(newImages), nil - } +// LoadAllImageFromArchive loads all images from the archive of multi-image that inputFile points to. +func (r *Runtime) LoadAllImageFromArchive(ctx context.Context, writer io.Writer, inputFile, signaturePolicy string) (string, error) { + newImages, err := r.ImageRuntime().LoadAllImagesFromDockerArchive(ctx, inputFile, signaturePolicy, writer) + if err == nil { + return getImageNames(newImages), nil } + return "", err +} +// LoadImageFromSingleImageArchive load image from the archive of single image that inputFile points to. +func (r *Runtime) LoadImageFromSingleImageArchive(ctx context.Context, writer io.Writer, inputFile, signaturePolicy string) (string, error) { + var err error for _, referenceFn := range []func() (types.ImageReference, error){ func() (types.ImageReference, error) { return dockerarchive.ParseReference(inputFile) }, func() (types.ImageReference, error) { - return ociarchive.NewReference(inputFile, name) // name may be "" - }, - func() (types.ImageReference, error) { - // prepend "localhost/" to support local image saved with this semantics - if !strings.Contains(name, "/") { - return ociarchive.NewReference(inputFile, fmt.Sprintf("%s/%s", image.DefaultLocalRegistry, name)) - } - return nil, nil + return ociarchive.NewReference(inputFile, "") }, func() (types.ImageReference, error) { return directory.NewReference(inputFile) }, func() (types.ImageReference, error) { - return layout.NewReference(inputFile, name) - }, - func() (types.ImageReference, error) { - // prepend "localhost/" to support local image saved with this semantics - if !strings.Contains(name, "/") { - return layout.NewReference(inputFile, fmt.Sprintf("%s/%s", image.DefaultLocalRegistry, name)) - } - return nil, nil + return layout.NewReference(inputFile, "") }, } { - src, err = referenceFn() + src, err := referenceFn() if err == nil && src != nil { - if newImages, err = r.ImageRuntime().LoadFromArchiveReference(ctx, src, signaturePolicy, writer); err == nil { + if newImages, err := r.ImageRuntime().LoadFromArchiveReference(ctx, src, signaturePolicy, writer); err == nil { return getImageNames(newImages), nil } } } - return "", errors.Wrapf(err, "error pulling %q", name) + return "", errors.Wrapf(err, "error pulling image") } func getImageNames(images []*image.Image) string { diff --git a/libpod/runtime_migrate.go b/libpod/runtime_migrate.go index 1ad32fe9c..f0f800ef0 100644 --- a/libpod/runtime_migrate.go +++ b/libpod/runtime_migrate.go @@ -18,9 +18,9 @@ import ( "github.com/sirupsen/logrus" ) -func stopPauseProcess() error { +func (r *Runtime) stopPauseProcess() error { if rootless.IsRootless() { - pausePidPath, err := util.GetRootlessPauseProcessPidPath() + pausePidPath, err := util.GetRootlessPauseProcessPidPathGivenDir(r.config.Engine.TmpDir) if err != nil { return errors.Wrapf(err, "could not get pause process pid file path") } @@ -98,5 +98,5 @@ func (r *Runtime) migrate(ctx context.Context) error { } } - return stopPauseProcess() + return r.stopPauseProcess() } diff --git a/libpod/runtime_migrate_unsupported.go b/libpod/runtime_migrate_unsupported.go index e362cca63..a9d351318 100644 --- a/libpod/runtime_migrate_unsupported.go +++ b/libpod/runtime_migrate_unsupported.go @@ -10,6 +10,6 @@ func (r *Runtime) migrate(ctx context.Context) error { return nil } -func stopPauseProcess() error { +func (r *Runtime) stopPauseProcess() error { return 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/images.go b/pkg/api/handlers/compat/images.go index d177b2335..a51dd8ed3 100644 --- a/pkg/api/handlers/compat/images.go +++ b/pkg/api/handlers/compat/images.go @@ -390,7 +390,7 @@ func LoadImages(w http.ResponseWriter, r *http.Request) { utils.Error(w, "Something went wrong.", http.StatusInternalServerError, errors.Wrap(err, "failed to write temporary file")) return } - id, err := runtime.LoadImage(r.Context(), "", f.Name(), writer, "") + id, err := runtime.LoadImage(r.Context(), f.Name(), writer, "") if err != nil { utils.Error(w, "Something went wrong.", http.StatusInternalServerError, errors.Wrap(err, "failed to load image")) return diff --git a/pkg/api/handlers/compat/images_build.go b/pkg/api/handlers/compat/images_build.go index a4bb72140..149050209 100644 --- a/pkg/api/handlers/compat/images_build.go +++ b/pkg/api/handlers/compat/images_build.go @@ -104,9 +104,6 @@ func BuildImage(w http.ResponseWriter, r *http.Request) { if len(query.Tag) > 0 { output = query.Tag[0] } - if _, found := r.URL.Query()["target"]; found { - output = query.Target - } var additionalNames []string if len(query.Tag) > 1 { @@ -162,7 +159,6 @@ func BuildImage(w http.ResponseWriter, r *http.Request) { reporter := channel.NewWriter(make(chan []byte, 1)) defer reporter.Close() - buildOptions := imagebuildah.BuildOptions{ ContextDirectory: contextDirectory, PullPolicy: pullPolicy, diff --git a/pkg/api/handlers/compat/networks.go b/pkg/api/handlers/compat/networks.go index c74cdb840..b4f3aa2f1 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 := getNetworkResourceByNameOrID(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 getNetworkResourceByNameOrID(nameOrID string, runtime *libpod.Runtime, filters map[string][]string) (*types.NetworkResource, error) { var ( ipamConfigs []dockerNetwork.IPAMConfig ) @@ -68,7 +68,7 @@ func getNetworkResourceByName(name string, runtime *libpod.Runtime) (*types.Netw } containerEndpoints := map[string]types.EndpointResource{} // Get the network path so we can get created time - networkConfigPath, err := network.GetCNIConfigPathByName(config, name) + networkConfigPath, err := network.GetCNIConfigPathByNameOrID(config, nameOrID) if err != nil { return nil, err } @@ -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) @@ -106,7 +116,7 @@ func getNetworkResourceByName(name string, runtime *libpod.Runtime) (*types.Netw if err != nil { return nil, err } - if netData, ok := data.NetworkSettings.Networks[name]; ok { + if netData, ok := data.NetworkSettings.Networks[conf.Name]; ok { containerEndpoint := types.EndpointResource{ Name: netData.NetworkID, EndpointID: netData.EndpointID, @@ -118,8 +128,8 @@ func getNetworkResourceByName(name string, runtime *libpod.Runtime) (*types.Netw } } report := types.NetworkResource{ - Name: name, - ID: name, + Name: conf.Name, + ID: network.GetNetworkID(conf.Name), Created: time.Unix(int64(stat.Ctim.Sec), int64(stat.Ctim.Nsec)), // nolint: unconvert Scope: "", Driver: 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 := getNetworkResourceByNameOrID(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/images.go b/pkg/api/handlers/libpod/images.go index be5a394de..6145207ca 100644 --- a/pkg/api/handlers/libpod/images.go +++ b/pkg/api/handlers/libpod/images.go @@ -336,7 +336,7 @@ func ImagesLoad(w http.ResponseWriter, r *http.Request) { } tmpfile.Close() - loadedImage, err := runtime.LoadImage(context.Background(), query.Reference, tmpfile.Name(), os.Stderr, "") + loadedImage, err := runtime.LoadImage(context.Background(), tmpfile.Name(), os.Stderr, "") if err != nil { utils.Error(w, "Something went wrong.", http.StatusInternalServerError, errors.Wrap(err, "unable to load image")) return 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..e6c85d244 100644 --- a/pkg/api/server/register_networks.go +++ b/pkg/api/server/register_networks.go @@ -65,7 +65,12 @@ 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). + // - id=[id] Matches for full or partial ID. + // - 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 +221,15 @@ 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). + // - id=[id] Matches for full or partial ID. + // - 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..65a110fd9 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,10 +40,13 @@ type NetworkCreateOptions struct { Driver string Gateway net.IP Internal bool + Labels map[string]string MacVLAN string Range net.IPNet Subnet net.IPNet IPv6 bool + // Mapping of driver options and values. + Options map[string]string } // NetworkCreateReport describes a created network for the cli diff --git a/pkg/domain/infra/abi/images.go b/pkg/domain/infra/abi/images.go index ef0e15264..1b523f06a 100644 --- a/pkg/domain/infra/abi/images.go +++ b/pkg/domain/infra/abi/images.go @@ -458,7 +458,7 @@ func (ir *ImageEngine) Load(ctx context.Context, opts entities.ImageLoadOptions) if !opts.Quiet { writer = os.Stderr } - name, err := ir.Libpod.LoadImage(ctx, opts.Name, opts.Input, writer, opts.SignaturePolicy) + name, err := ir.Libpod.LoadImage(ctx, opts.Input, writer, opts.SignaturePolicy) if err != nil { return nil, err } 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/pkg/domain/infra/abi/system.go b/pkg/domain/infra/abi/system.go index 72fd98ac1..ec2532bea 100644 --- a/pkg/domain/infra/abi/system.go +++ b/pkg/domain/infra/abi/system.go @@ -11,6 +11,7 @@ import ( "strings" "github.com/containers/common/pkg/config" + "github.com/containers/podman/v2/libpod" "github.com/containers/podman/v2/libpod/define" "github.com/containers/podman/v2/pkg/cgroups" "github.com/containers/podman/v2/pkg/domain/entities" @@ -86,7 +87,11 @@ func (ic *ContainerEngine) SetupRootless(_ context.Context, cmd *cobra.Command) return nil } - pausePidPath, err := util.GetRootlessPauseProcessPidPath() + tmpDir, err := ic.Libpod.TmpDir() + if err != nil { + return err + } + pausePidPath, err := util.GetRootlessPauseProcessPidPathGivenDir(tmpDir) if err != nil { return errors.Wrapf(err, "could not get pause process pid file path") } @@ -112,7 +117,7 @@ func (ic *ContainerEngine) SetupRootless(_ context.Context, cmd *cobra.Command) } became, ret, err = rootless.TryJoinFromFilePaths(pausePidPath, true, paths) - if err := movePauseProcessToScope(); err != nil { + if err := movePauseProcessToScope(ic.Libpod); err != nil { conf, err := ic.Config(context.Background()) if err != nil { return err @@ -133,8 +138,12 @@ func (ic *ContainerEngine) SetupRootless(_ context.Context, cmd *cobra.Command) return nil } -func movePauseProcessToScope() error { - pausePidPath, err := util.GetRootlessPauseProcessPidPath() +func movePauseProcessToScope(r *libpod.Runtime) error { + tmpDir, err := r.TmpDir() + if err != nil { + return err + } + pausePidPath, err := util.GetRootlessPauseProcessPidPathGivenDir(tmpDir) if err != nil { return errors.Wrapf(err, "could not get pause process pid file path") } diff --git a/pkg/specgen/generate/container_create.go b/pkg/specgen/generate/container_create.go index 45a374216..95e4eeb8f 100644 --- a/pkg/specgen/generate/container_create.go +++ b/pkg/specgen/generate/container_create.go @@ -388,7 +388,7 @@ func CreateExitCommandArgs(storageConfig storage.StoreOptions, config *config.Co } if syslog { - command = append(command, "--syslog", "true") + command = append(command, "--syslog") } command = append(command, []string{"container", "cleanup"}...) diff --git a/pkg/util/utils_supported.go b/pkg/util/utils_supported.go index 2d636a7cb..a63c76415 100644 --- a/pkg/util/utils_supported.go +++ b/pkg/util/utils_supported.go @@ -99,7 +99,8 @@ func GetRootlessConfigHomeDir() (string, error) { } // GetRootlessPauseProcessPidPath returns the path to the file that holds the pid for -// the pause process +// the pause process. +// DEPRECATED - switch to GetRootlessPauseProcessPidPathGivenDir func GetRootlessPauseProcessPidPath() (string, error) { runtimeDir, err := GetRuntimeDir() if err != nil { @@ -107,3 +108,13 @@ func GetRootlessPauseProcessPidPath() (string, error) { } return filepath.Join(runtimeDir, "libpod", "pause.pid"), nil } + +// GetRootlessPauseProcessPidPathGivenDir returns the path to the file that +// holds the PID of the pause process, given the location of Libpod's temporary +// files. +func GetRootlessPauseProcessPidPathGivenDir(libpodTmpDir string) (string, error) { + if libpodTmpDir == "" { + return "", errors.Errorf("must provide non-empty tmporary directory") + } + return filepath.Join(libpodTmpDir, "pause.pid"), nil +} diff --git a/pkg/util/utils_windows.go b/pkg/util/utils_windows.go index 9bba2d1ee..46ca5e7f1 100644 --- a/pkg/util/utils_windows.go +++ b/pkg/util/utils_windows.go @@ -25,6 +25,12 @@ func GetRootlessPauseProcessPidPath() (string, error) { return "", errors.Wrap(errNotImplemented, "GetRootlessPauseProcessPidPath") } +// GetRootlessPauseProcessPidPath returns the path to the file that holds the pid for +// the pause process +func GetRootlessPauseProcessPidPathGivenDir(unused string) (string, error) { + return "", errors.Wrap(errNotImplemented, "GetRootlessPauseProcessPidPath") +} + // GetRuntimeDir returns the runtime directory func GetRuntimeDir() (string, error) { return "", errors.New("this function is not implemented for windows") 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..0ce56ee3c 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,22 @@ 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 +# id filter filters={"id":["a7662f44d65029fd4635c91feea3d720a57cef52e2a9fcc7772b69072cc1ccd1"]} +t GET networks?filters=%7B%22id%22%3A%5B%22a7662f44d65029fd4635c91feea3d720a57cef52e2a9fcc7772b69072cc1ccd1%22%5D%7D 200 \ +length=1 \ +.[0].Name=network1 \ +.[0].Id=a7662f44d65029fd4635c91feea3d720a57cef52e2a9fcc7772b69072cc1ccd1 +# invalid filter filters={"dangling":["1"]} +t GET networks?filters=%7B%22dangling%22%3A%5B%221%22%5D%7D 500 \ +.cause='invalid filter "dangling"' + +# network inspect docker +t GET networks/a7662f44d65029fd4635c91feea3d720a57cef52e2a9fcc7772b69072cc1ccd1 200 \ +.Name=network1 \ +.Id=a7662f44d65029fd4635c91feea3d720a57cef52e2a9fcc7772b69072cc1ccd1 # clean the network t DELETE libpod/networks/network1 200 \ diff --git a/test/e2e/common_test.go b/test/e2e/common_test.go index f107fe76b..16d8bb770 100644 --- a/test/e2e/common_test.go +++ b/test/e2e/common_test.go @@ -788,3 +788,9 @@ func generateNetworkConfig(p *PodmanTestIntegration) (string, string) { return name, path } + +func (p *PodmanTestIntegration) removeCNINetwork(name string) { + session := p.Podman([]string{"network", "rm", "-f", name}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(BeNumerically("<=", 1)) +} diff --git a/test/e2e/containers_conf_test.go b/test/e2e/containers_conf_test.go index 906153c0f..866162f7f 100644 --- a/test/e2e/containers_conf_test.go +++ b/test/e2e/containers_conf_test.go @@ -203,35 +203,35 @@ var _ = Describe("Podman run", func() { session := podmanTest.Podman([]string{"run", ALPINE, "cat", "/etc/resolv.conf"}) session.WaitWithDefaultTimeout() Expect(session.ExitCode()).To(Equal(0)) - session.LineInOuputStartsWith("search foobar.com") + session.LineInOutputStartsWith("search foobar.com") }) It("podman run add dns server", func() { session := podmanTest.Podman([]string{"run", ALPINE, "cat", "/etc/resolv.conf"}) session.WaitWithDefaultTimeout() Expect(session.ExitCode()).To(Equal(0)) - session.LineInOuputStartsWith("server 1.2.3.4") + session.LineInOutputStartsWith("server 1.2.3.4") }) It("podman run add dns option", func() { session := podmanTest.Podman([]string{"run", ALPINE, "cat", "/etc/resolv.conf"}) session.WaitWithDefaultTimeout() Expect(session.ExitCode()).To(Equal(0)) - session.LineInOuputStartsWith("options debug") + session.LineInOutputStartsWith("options debug") }) It("podman run containers.conf remove all search domain", func() { session := podmanTest.Podman([]string{"run", "--dns-search=.", ALPINE, "cat", "/etc/resolv.conf"}) session.WaitWithDefaultTimeout() Expect(session.ExitCode()).To(Equal(0)) - Expect(session.LineInOuputStartsWith("search")).To(BeFalse()) + Expect(session.LineInOutputStartsWith("search")).To(BeFalse()) }) It("podman run use containers.conf search domain", func() { session := podmanTest.Podman([]string{"run", ALPINE, "cat", "/etc/resolv.conf"}) session.WaitWithDefaultTimeout() Expect(session.ExitCode()).To(Equal(0)) - Expect(session.LineInOuputStartsWith("search")).To(BeTrue()) + Expect(session.LineInOutputStartsWith("search")).To(BeTrue()) Expect(session.OutputToString()).To(ContainSubstring("foobar.com")) Expect(session.OutputToString()).To(ContainSubstring("1.2.3.4")) @@ -275,7 +275,7 @@ var _ = Describe("Podman run", func() { session = podmanTest.Podman([]string{"run", ALPINE, "cat", "/etc/resolv.conf"}) session.WaitWithDefaultTimeout() Expect(session.ExitCode()).To(Equal(0)) - Expect(session.LineInOuputStartsWith("search")).To(BeTrue()) + Expect(session.LineInOutputStartsWith("search")).To(BeTrue()) Expect(session.OutputToString()).To(ContainSubstring("foobar.com")) Expect(session.OutputToString()).To(ContainSubstring("1.2.3.4")) Expect(session.OutputToString()).To(ContainSubstring("debug")) 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/images_test.go b/test/e2e/images_test.go index 4c65a85d5..281b2c313 100644 --- a/test/e2e/images_test.go +++ b/test/e2e/images_test.go @@ -41,8 +41,8 @@ var _ = Describe("Podman images", func() { session.WaitWithDefaultTimeout() Expect(session).Should(Exit(0)) Expect(len(session.OutputToStringArray())).To(BeNumerically(">", 2)) - Expect(session.LineInOuputStartsWith("quay.io/libpod/alpine")).To(BeTrue()) - Expect(session.LineInOuputStartsWith("quay.io/libpod/busybox")).To(BeTrue()) + Expect(session.LineInOutputStartsWith("quay.io/libpod/alpine")).To(BeTrue()) + Expect(session.LineInOutputStartsWith("quay.io/libpod/busybox")).To(BeTrue()) }) It("podman image List", func() { @@ -50,8 +50,8 @@ var _ = Describe("Podman images", func() { session.WaitWithDefaultTimeout() Expect(session).Should(Exit(0)) Expect(len(session.OutputToStringArray())).To(BeNumerically(">", 2)) - Expect(session.LineInOuputStartsWith("quay.io/libpod/alpine")).To(BeTrue()) - Expect(session.LineInOuputStartsWith("quay.io/libpod/busybox")).To(BeTrue()) + Expect(session.LineInOutputStartsWith("quay.io/libpod/alpine")).To(BeTrue()) + Expect(session.LineInOutputStartsWith("quay.io/libpod/busybox")).To(BeTrue()) }) It("podman images with multiple tags", func() { @@ -86,8 +86,8 @@ var _ = Describe("Podman images", func() { session.WaitWithDefaultTimeout() Expect(session).Should(Exit(0)) Expect(len(session.OutputToStringArray())).To(BeNumerically(">", 2)) - Expect(session.LineInOuputStartsWith("quay.io/libpod/alpine")).To(BeTrue()) - Expect(session.LineInOuputStartsWith("quay.io/libpod/busybox")).To(BeTrue()) + Expect(session.LineInOutputStartsWith("quay.io/libpod/alpine")).To(BeTrue()) + Expect(session.LineInOutputStartsWith("quay.io/libpod/busybox")).To(BeTrue()) }) It("podman empty images list in JSON format", func() { @@ -278,7 +278,7 @@ WORKDIR /test It("podman images sort by values", func() { sortValueTest := func(value string, result int, format string) []string { f := fmt.Sprintf("{{.%s}}", format) - session := podmanTest.Podman([]string{"images", "--sort", value, "--format", f}) + session := podmanTest.Podman([]string{"images", "--noheading", "--sort", value, "--format", f}) session.WaitWithDefaultTimeout() Expect(session).Should(Exit(result)) diff --git a/test/e2e/import_test.go b/test/e2e/import_test.go index 1be4ef920..bd465bf38 100644 --- a/test/e2e/import_test.go +++ b/test/e2e/import_test.go @@ -87,7 +87,7 @@ var _ = Describe("Podman import", func() { results := podmanTest.Podman([]string{"history", "imported-image", "--format", "{{.Comment}}"}) results.WaitWithDefaultTimeout() Expect(results.ExitCode()).To(Equal(0)) - Expect(results.LineInOuputStartsWith("importing container test message")).To(BeTrue()) + Expect(results.LineInOutputStartsWith("importing container test message")).To(BeTrue()) }) It("podman import with change flag CMD=<path>", func() { diff --git a/test/e2e/network_connect_disconnect_test.go b/test/e2e/network_connect_disconnect_test.go new file mode 100644 index 000000000..7cdad9bf2 --- /dev/null +++ b/test/e2e/network_connect_disconnect_test.go @@ -0,0 +1,217 @@ +package integration + +import ( + "os" + + . "github.com/containers/podman/v2/test/utils" + "github.com/containers/storage/pkg/stringid" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("Podman network connect and disconnect", func() { + var ( + tempdir string + err error + podmanTest *PodmanTestIntegration + ) + + BeforeEach(func() { + tempdir, err = CreateTempDirInTempDir() + if err != nil { + os.Exit(1) + } + podmanTest = PodmanTestCreate(tempdir) + podmanTest.Setup() + }) + + AfterEach(func() { + podmanTest.Cleanup() + f := CurrentGinkgoTestDescription() + processTestResult(f) + + }) + + It("bad network name in disconnect should result in error", func() { + SkipIfRootless("network connect and disconnect are only rootfull") + dis := podmanTest.Podman([]string{"network", "disconnect", "foobar", "test"}) + dis.WaitWithDefaultTimeout() + Expect(dis.ExitCode()).ToNot(BeZero()) + + }) + + It("bad container name in network disconnect should result in error", func() { + SkipIfRootless("network connect and disconnect are only rootfull") + netName := "aliasTest" + stringid.GenerateNonCryptoID() + session := podmanTest.Podman([]string{"network", "create", netName}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(BeZero()) + defer podmanTest.removeCNINetwork(netName) + + dis := podmanTest.Podman([]string{"network", "disconnect", netName, "foobar"}) + dis.WaitWithDefaultTimeout() + Expect(dis.ExitCode()).ToNot(BeZero()) + + }) + + It("podman network disconnect", func() { + SkipIfRootless("network connect and disconnect are only rootfull") + netName := "aliasTest" + stringid.GenerateNonCryptoID() + session := podmanTest.Podman([]string{"network", "create", netName}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(BeZero()) + defer podmanTest.removeCNINetwork(netName) + + ctr := podmanTest.Podman([]string{"run", "-dt", "--name", "test", "--network", netName, ALPINE, "top"}) + ctr.WaitWithDefaultTimeout() + Expect(ctr.ExitCode()).To(BeZero()) + + exec := podmanTest.Podman([]string{"exec", "-it", "test", "ip", "addr", "show", "eth0"}) + exec.WaitWithDefaultTimeout() + Expect(exec.ExitCode()).To(BeZero()) + + dis := podmanTest.Podman([]string{"network", "disconnect", netName, "test"}) + dis.WaitWithDefaultTimeout() + Expect(dis.ExitCode()).To(BeZero()) + + exec = podmanTest.Podman([]string{"exec", "-it", "test", "ip", "addr", "show", "eth0"}) + exec.WaitWithDefaultTimeout() + Expect(exec.ExitCode()).ToNot(BeZero()) + }) + + It("bad network name in connect should result in error", func() { + SkipIfRootless("network connect and disconnect are only rootfull") + dis := podmanTest.Podman([]string{"network", "connect", "foobar", "test"}) + dis.WaitWithDefaultTimeout() + Expect(dis.ExitCode()).ToNot(BeZero()) + + }) + + It("bad container name in network connect should result in error", func() { + SkipIfRootless("network connect and disconnect are only rootfull") + netName := "aliasTest" + stringid.GenerateNonCryptoID() + session := podmanTest.Podman([]string{"network", "create", netName}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(BeZero()) + defer podmanTest.removeCNINetwork(netName) + + dis := podmanTest.Podman([]string{"network", "connect", netName, "foobar"}) + dis.WaitWithDefaultTimeout() + Expect(dis.ExitCode()).ToNot(BeZero()) + + }) + + It("podman connect on a container that already is connected to the network should error", func() { + SkipIfRootless("network connect and disconnect are only rootfull") + netName := "aliasTest" + stringid.GenerateNonCryptoID() + session := podmanTest.Podman([]string{"network", "create", netName}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(BeZero()) + defer podmanTest.removeCNINetwork(netName) + + ctr := podmanTest.Podman([]string{"create", "--name", "test", "--network", netName, ALPINE, "top"}) + ctr.WaitWithDefaultTimeout() + Expect(ctr.ExitCode()).To(BeZero()) + + con := podmanTest.Podman([]string{"network", "connect", netName, "test"}) + con.WaitWithDefaultTimeout() + Expect(con.ExitCode()).ToNot(BeZero()) + }) + + It("podman network connect", func() { + SkipIfRemote("This requires a pending PR to be merged before it will work") + SkipIfRootless("network connect and disconnect are only rootfull") + netName := "aliasTest" + stringid.GenerateNonCryptoID() + session := podmanTest.Podman([]string{"network", "create", netName}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(BeZero()) + defer podmanTest.removeCNINetwork(netName) + + ctr := podmanTest.Podman([]string{"run", "-dt", "--name", "test", "--network", netName, ALPINE, "top"}) + ctr.WaitWithDefaultTimeout() + Expect(ctr.ExitCode()).To(BeZero()) + + exec := podmanTest.Podman([]string{"exec", "-it", "test", "ip", "addr", "show", "eth0"}) + exec.WaitWithDefaultTimeout() + Expect(exec.ExitCode()).To(BeZero()) + + // Create a second network + newNetName := "aliasTest" + stringid.GenerateNonCryptoID() + session = podmanTest.Podman([]string{"network", "create", newNetName}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(BeZero()) + defer podmanTest.removeCNINetwork(newNetName) + + connect := podmanTest.Podman([]string{"network", "connect", newNetName, "test"}) + connect.WaitWithDefaultTimeout() + Expect(connect.ExitCode()).To(BeZero()) + + exec = podmanTest.Podman([]string{"exec", "-it", "test", "ip", "addr", "show", "eth1"}) + exec.WaitWithDefaultTimeout() + Expect(exec.ExitCode()).To(BeZero()) + }) + + It("podman network connect when not running", func() { + SkipIfRootless("network connect and disconnect are only rootfull") + netName := "aliasTest" + stringid.GenerateNonCryptoID() + session := podmanTest.Podman([]string{"network", "create", netName}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(BeZero()) + defer podmanTest.removeCNINetwork(netName) + + ctr := podmanTest.Podman([]string{"create", "--name", "test", ALPINE, "top"}) + ctr.WaitWithDefaultTimeout() + Expect(ctr.ExitCode()).To(BeZero()) + + dis := podmanTest.Podman([]string{"network", "connect", netName, "test"}) + dis.WaitWithDefaultTimeout() + Expect(dis.ExitCode()).To(BeZero()) + + start := podmanTest.Podman([]string{"start", "test"}) + start.WaitWithDefaultTimeout() + Expect(start.ExitCode()).To(BeZero()) + + exec := podmanTest.Podman([]string{"exec", "-it", "test", "ip", "addr", "show", "eth0"}) + exec.WaitWithDefaultTimeout() + Expect(exec.ExitCode()).To(BeZero()) + + exec = podmanTest.Podman([]string{"exec", "-it", "test", "ip", "addr", "show", "eth1"}) + exec.WaitWithDefaultTimeout() + Expect(exec.ExitCode()).To(BeZero()) + }) + + It("podman network disconnect when not running", func() { + SkipIfRootless("network connect and disconnect are only rootfull") + netName1 := "aliasTest" + stringid.GenerateNonCryptoID() + session := podmanTest.Podman([]string{"network", "create", netName1}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(BeZero()) + defer podmanTest.removeCNINetwork(netName1) + + netName2 := "aliasTest" + stringid.GenerateNonCryptoID() + session2 := podmanTest.Podman([]string{"network", "create", netName2}) + session2.WaitWithDefaultTimeout() + Expect(session2.ExitCode()).To(BeZero()) + defer podmanTest.removeCNINetwork(netName2) + + ctr := podmanTest.Podman([]string{"create", "--name", "test", "--network", netName1 + "," + netName2, ALPINE, "top"}) + ctr.WaitWithDefaultTimeout() + Expect(ctr.ExitCode()).To(BeZero()) + + dis := podmanTest.Podman([]string{"network", "disconnect", netName1, "test"}) + dis.WaitWithDefaultTimeout() + Expect(dis.ExitCode()).To(BeZero()) + + start := podmanTest.Podman([]string{"start", "test"}) + start.WaitWithDefaultTimeout() + Expect(start.ExitCode()).To(BeZero()) + + exec := podmanTest.Podman([]string{"exec", "-it", "test", "ip", "addr", "show", "eth0"}) + exec.WaitWithDefaultTimeout() + Expect(exec.ExitCode()).To(BeZero()) + + exec = podmanTest.Podman([]string{"exec", "-it", "test", "ip", "addr", "show", "eth1"}) + exec.WaitWithDefaultTimeout() + Expect(exec.ExitCode()).ToNot(BeZero()) + }) +}) diff --git a/test/e2e/network_create_test.go b/test/e2e/network_create_test.go index cb997d10a..21b3074fc 100644 --- a/test/e2e/network_create_test.go +++ b/test/e2e/network_create_test.go @@ -55,12 +55,6 @@ func genericPluginsToPortMap(plugins interface{}, pluginType string) (network.Po return portMap, err } -func (p *PodmanTestIntegration) removeCNINetwork(name string) { - session := p.Podman([]string{"network", "rm", "-f", name}) - session.WaitWithDefaultTimeout() - Expect(session.ExitCode()).To(BeNumerically("<=", 1)) -} - func removeNetworkDevice(name string) { session := SystemExec("ip", []string{"link", "delete", name}) session.WaitWithDefaultTimeout() @@ -335,4 +329,37 @@ var _ = Describe("Podman network create", func() { Expect(nc).To(ExitWithError()) }) + It("podman network create with mtu option", func() { + net := "mtu-test" + nc := podmanTest.Podman([]string{"network", "create", "--opt", "mtu=9000", net}) + nc.WaitWithDefaultTimeout() + Expect(nc.ExitCode()).To(BeZero()) + defer podmanTest.removeCNINetwork(net) + + nc = podmanTest.Podman([]string{"network", "inspect", net}) + nc.WaitWithDefaultTimeout() + Expect(nc.ExitCode()).To(BeZero()) + Expect(nc.OutputToString()).To(ContainSubstring(`"mtu": 9000,`)) + }) + + It("podman network create with vlan option", func() { + net := "vlan-test" + nc := podmanTest.Podman([]string{"network", "create", "--opt", "vlan=9", net}) + nc.WaitWithDefaultTimeout() + Expect(nc.ExitCode()).To(BeZero()) + defer podmanTest.removeCNINetwork(net) + + nc = podmanTest.Podman([]string{"network", "inspect", net}) + nc.WaitWithDefaultTimeout() + Expect(nc.ExitCode()).To(BeZero()) + Expect(nc.OutputToString()).To(ContainSubstring(`"vlan": 9`)) + }) + + It("podman network create with invalid option", func() { + net := "invalid-test" + nc := podmanTest.Podman([]string{"network", "create", "--opt", "foo=bar", net}) + nc.WaitWithDefaultTimeout() + Expect(nc).To(ExitWithError()) + }) + }) diff --git a/test/e2e/network_test.go b/test/e2e/network_test.go index adcf74f7e..ffc914bc2 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) @@ -76,6 +135,40 @@ var _ = Describe("Podman network", func() { Expect(session.LineInOutputContains(name)).To(BeFalse()) }) + It("podman network ID test", func() { + net := "networkIDTest" + // the network id should be the sha256 hash of the network name + netID := "6073aefe03cdf8f29be5b23ea9795c431868a3a22066a6290b187691614fee84" + session := podmanTest.Podman([]string{"network", "create", net}) + session.WaitWithDefaultTimeout() + defer podmanTest.removeCNINetwork(net) + Expect(session.ExitCode()).To(BeZero()) + + session = podmanTest.Podman([]string{"network", "ls", "--format", "{{.Name}} {{.ID}}", "--filter", "id=" + netID}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(BeZero()) + Expect(session.OutputToString()).To(ContainSubstring(net + " " + netID[:12])) + + session = podmanTest.Podman([]string{"network", "ls", "--format", "{{.Name}} {{.ID}}", "--filter", "id=" + netID[10:50], "--no-trunc"}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(BeZero()) + Expect(session.OutputToString()).To(ContainSubstring(net + " " + netID)) + + session = podmanTest.Podman([]string{"network", "inspect", netID[:40]}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(BeZero()) + Expect(session.OutputToString()).To(ContainSubstring(net)) + + session = podmanTest.Podman([]string{"network", "inspect", netID[1:]}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).ToNot(BeZero()) + Expect(session.ErrorToString()).To(ContainSubstring("no such network")) + + session = podmanTest.Podman([]string{"network", "rm", netID}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(BeZero()) + }) + rm_func := func(rm string) { It(fmt.Sprintf("podman network %s no args", rm), func() { session := podmanTest.Podman([]string{"network", rm}) @@ -347,159 +440,6 @@ var _ = Describe("Podman network", func() { Expect(c3.ExitCode()).To(BeZero()) }) - It("bad network name in disconnect should result in error", func() { - SkipIfRootless("network connect and disconnect are only rootfull") - dis := podmanTest.Podman([]string{"network", "disconnect", "foobar", "test"}) - dis.WaitWithDefaultTimeout() - Expect(dis.ExitCode()).ToNot(BeZero()) - - }) - - It("bad container name in network disconnect should result in error", func() { - SkipIfRootless("network connect and disconnect are only rootfull") - netName := "aliasTest" + stringid.GenerateNonCryptoID() - session := podmanTest.Podman([]string{"network", "create", netName}) - session.WaitWithDefaultTimeout() - Expect(session.ExitCode()).To(BeZero()) - defer podmanTest.removeCNINetwork(netName) - - dis := podmanTest.Podman([]string{"network", "disconnect", netName, "foobar"}) - dis.WaitWithDefaultTimeout() - Expect(dis.ExitCode()).ToNot(BeZero()) - - }) - - It("podman network disconnect with invalid container state should result in error", func() { - SkipIfRootless("network connect and disconnect are only rootfull") - netName := "aliasTest" + stringid.GenerateNonCryptoID() - session := podmanTest.Podman([]string{"network", "create", netName}) - session.WaitWithDefaultTimeout() - Expect(session.ExitCode()).To(BeZero()) - defer podmanTest.removeCNINetwork(netName) - - ctr := podmanTest.Podman([]string{"create", "--name", "test", "--network", netName, ALPINE, "top"}) - ctr.WaitWithDefaultTimeout() - Expect(ctr.ExitCode()).To(BeZero()) - - dis := podmanTest.Podman([]string{"network", "disconnect", netName, "test"}) - dis.WaitWithDefaultTimeout() - Expect(dis.ExitCode()).ToNot(BeZero()) - }) - - It("podman network disconnect", func() { - SkipIfRootless("network connect and disconnect are only rootfull") - netName := "aliasTest" + stringid.GenerateNonCryptoID() - session := podmanTest.Podman([]string{"network", "create", netName}) - session.WaitWithDefaultTimeout() - Expect(session.ExitCode()).To(BeZero()) - defer podmanTest.removeCNINetwork(netName) - - ctr := podmanTest.Podman([]string{"run", "-dt", "--name", "test", "--network", netName, ALPINE, "top"}) - ctr.WaitWithDefaultTimeout() - Expect(ctr.ExitCode()).To(BeZero()) - - exec := podmanTest.Podman([]string{"exec", "-it", "test", "ip", "addr", "show", "eth0"}) - exec.WaitWithDefaultTimeout() - Expect(exec.ExitCode()).To(BeZero()) - - dis := podmanTest.Podman([]string{"network", "disconnect", netName, "test"}) - dis.WaitWithDefaultTimeout() - Expect(dis.ExitCode()).To(BeZero()) - - exec = podmanTest.Podman([]string{"exec", "-it", "test", "ip", "addr", "show", "eth0"}) - exec.WaitWithDefaultTimeout() - Expect(exec.ExitCode()).ToNot(BeZero()) - }) - - It("bad network name in connect should result in error", func() { - SkipIfRootless("network connect and disconnect are only rootfull") - dis := podmanTest.Podman([]string{"network", "connect", "foobar", "test"}) - dis.WaitWithDefaultTimeout() - Expect(dis.ExitCode()).ToNot(BeZero()) - - }) - - It("bad container name in network connect should result in error", func() { - SkipIfRootless("network connect and disconnect are only rootfull") - netName := "aliasTest" + stringid.GenerateNonCryptoID() - session := podmanTest.Podman([]string{"network", "create", netName}) - session.WaitWithDefaultTimeout() - Expect(session.ExitCode()).To(BeZero()) - defer podmanTest.removeCNINetwork(netName) - - dis := podmanTest.Podman([]string{"network", "connect", netName, "foobar"}) - dis.WaitWithDefaultTimeout() - Expect(dis.ExitCode()).ToNot(BeZero()) - - }) - - It("podman connect on a container that already is connected to the network should error", func() { - SkipIfRootless("network connect and disconnect are only rootfull") - netName := "aliasTest" + stringid.GenerateNonCryptoID() - session := podmanTest.Podman([]string{"network", "create", netName}) - session.WaitWithDefaultTimeout() - Expect(session.ExitCode()).To(BeZero()) - defer podmanTest.removeCNINetwork(netName) - - ctr := podmanTest.Podman([]string{"create", "--name", "test", "--network", netName, ALPINE, "top"}) - ctr.WaitWithDefaultTimeout() - Expect(ctr.ExitCode()).To(BeZero()) - - con := podmanTest.Podman([]string{"network", "connect", netName, "test"}) - con.WaitWithDefaultTimeout() - Expect(con.ExitCode()).ToNot(BeZero()) - }) - - It("podman network connect with invalid container state should result in error", func() { - SkipIfRootless("network connect and disconnect are only rootfull") - netName := "aliasTest" + stringid.GenerateNonCryptoID() - session := podmanTest.Podman([]string{"network", "create", netName}) - session.WaitWithDefaultTimeout() - Expect(session.ExitCode()).To(BeZero()) - defer podmanTest.removeCNINetwork(netName) - - ctr := podmanTest.Podman([]string{"create", "--name", "test", "--network", netName, ALPINE, "top"}) - ctr.WaitWithDefaultTimeout() - Expect(ctr.ExitCode()).To(BeZero()) - - dis := podmanTest.Podman([]string{"network", "connect", netName, "test"}) - dis.WaitWithDefaultTimeout() - Expect(dis.ExitCode()).ToNot(BeZero()) - }) - - It("podman network connect", func() { - SkipIfRemote("This requires a pending PR to be merged before it will work") - SkipIfRootless("network connect and disconnect are only rootfull") - netName := "aliasTest" + stringid.GenerateNonCryptoID() - session := podmanTest.Podman([]string{"network", "create", netName}) - session.WaitWithDefaultTimeout() - Expect(session.ExitCode()).To(BeZero()) - defer podmanTest.removeCNINetwork(netName) - - ctr := podmanTest.Podman([]string{"run", "-dt", "--name", "test", "--network", netName, ALPINE, "top"}) - ctr.WaitWithDefaultTimeout() - Expect(ctr.ExitCode()).To(BeZero()) - - exec := podmanTest.Podman([]string{"exec", "-it", "test", "ip", "addr", "show", "eth0"}) - exec.WaitWithDefaultTimeout() - Expect(exec.ExitCode()).To(BeZero()) - - // Create a second network - newNetName := "aliasTest" + stringid.GenerateNonCryptoID() - session = podmanTest.Podman([]string{"network", "create", newNetName}) - session.WaitWithDefaultTimeout() - Expect(session.ExitCode()).To(BeZero()) - defer podmanTest.removeCNINetwork(newNetName) - - connect := podmanTest.Podman([]string{"network", "connect", newNetName, "test"}) - connect.WaitWithDefaultTimeout() - Expect(connect.ExitCode()).To(BeZero()) - - exec = podmanTest.Podman([]string{"exec", "-it", "test", "ip", "addr", "show", "eth1"}) - exec.WaitWithDefaultTimeout() - Expect(exec.ExitCode()).To(BeZero()) - }) - It("podman network create/remove macvlan", func() { net := "macvlan" + stringid.GenerateNonCryptoID() nc := podmanTest.Podman([]string{"network", "create", "--macvlan", "lo", net}) diff --git a/test/e2e/port_test.go b/test/e2e/port_test.go index a3ce8bd69..4aaf2cbc1 100644 --- a/test/e2e/port_test.go +++ b/test/e2e/port_test.go @@ -61,7 +61,7 @@ var _ = Describe("Podman port", func() { result.WaitWithDefaultTimeout() Expect(result.ExitCode()).To(Equal(0)) port := strings.Split(result.OutputToStringArray()[0], ":")[1] - Expect(result.LineInOuputStartsWith(fmt.Sprintf("80/tcp -> 0.0.0.0:%s", port))).To(BeTrue()) + Expect(result.LineInOutputStartsWith(fmt.Sprintf("80/tcp -> 0.0.0.0:%s", port))).To(BeTrue()) }) It("podman container port -l nginx", func() { @@ -79,7 +79,7 @@ var _ = Describe("Podman port", func() { result.WaitWithDefaultTimeout() Expect(result.ExitCode()).To(Equal(0)) port := strings.Split(result.OutputToStringArray()[0], ":")[1] - Expect(result.LineInOuputStartsWith(fmt.Sprintf("80/tcp -> 0.0.0.0:%s", port))).To(BeTrue()) + Expect(result.LineInOutputStartsWith(fmt.Sprintf("80/tcp -> 0.0.0.0:%s", port))).To(BeTrue()) }) It("podman port -l port nginx", func() { @@ -97,7 +97,7 @@ var _ = Describe("Podman port", func() { result.WaitWithDefaultTimeout() Expect(result.ExitCode()).To(Equal(0)) port := strings.Split(result.OutputToStringArray()[0], ":")[1] - Expect(result.LineInOuputStartsWith(fmt.Sprintf("0.0.0.0:%s", port))).To(BeTrue()) + Expect(result.LineInOutputStartsWith(fmt.Sprintf("0.0.0.0:%s", port))).To(BeTrue()) }) It("podman port -a nginx", func() { @@ -124,7 +124,7 @@ var _ = Describe("Podman port", func() { result := podmanTest.Podman([]string{"port", "portcheck"}) result.WaitWithDefaultTimeout() Expect(result.ExitCode()).To(Equal(0)) - result.LineInOuputStartsWith("80/tcp -> 0.0.0.0:") + result.LineInOutputStartsWith("80/tcp -> 0.0.0.0:") }) It("podman port multiple ports", func() { @@ -142,12 +142,12 @@ var _ = Describe("Podman port", func() { result1 := podmanTest.Podman([]string{"port", "test", "5000"}) result1.WaitWithDefaultTimeout() Expect(result1.ExitCode()).To(BeZero()) - Expect(result1.LineInOuputStartsWith("0.0.0.0:5000")).To(BeTrue()) + Expect(result1.LineInOutputStartsWith("0.0.0.0:5000")).To(BeTrue()) // Check that the second port was honored result2 := podmanTest.Podman([]string{"port", "test", "5001"}) result2.WaitWithDefaultTimeout() Expect(result2.ExitCode()).To(BeZero()) - Expect(result2.LineInOuputStartsWith("0.0.0.0:5001")).To(BeTrue()) + Expect(result2.LineInOutputStartsWith("0.0.0.0:5001")).To(BeTrue()) }) }) diff --git a/test/e2e/pull_test.go b/test/e2e/pull_test.go index f1b055d6d..446e2bd38 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" @@ -384,7 +385,7 @@ var _ = Describe("Podman pull", func() { session := podmanTest.Podman([]string{"pull", "--all-tags", "k8s.gcr.io/pause"}) session.WaitWithDefaultTimeout() Expect(session.ExitCode()).To(Equal(0)) - Expect(session.LineInOuputStartsWith("Pulled Images:")).To(BeTrue()) + Expect(session.LineInOutputStartsWith("Pulled Images:")).To(BeTrue()) session = podmanTest.Podman([]string{"images"}) session.WaitWithDefaultTimeout() @@ -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/e2e/run_dns_test.go b/test/e2e/run_dns_test.go index ff018f5d8..c8996c5e8 100644 --- a/test/e2e/run_dns_test.go +++ b/test/e2e/run_dns_test.go @@ -36,14 +36,14 @@ var _ = Describe("Podman run dns", func() { session := podmanTest.Podman([]string{"run", "--dns-search=foobar.com", ALPINE, "cat", "/etc/resolv.conf"}) session.WaitWithDefaultTimeout() Expect(session.ExitCode()).To(Equal(0)) - session.LineInOuputStartsWith("search foobar.com") + session.LineInOutputStartsWith("search foobar.com") }) It("podman run remove all search domain", func() { session := podmanTest.Podman([]string{"run", "--dns-search=.", ALPINE, "cat", "/etc/resolv.conf"}) session.WaitWithDefaultTimeout() Expect(session.ExitCode()).To(Equal(0)) - Expect(session.LineInOuputStartsWith("search")).To(BeFalse()) + Expect(session.LineInOutputStartsWith("search")).To(BeFalse()) }) It("podman run add bad dns server", func() { @@ -56,14 +56,14 @@ var _ = Describe("Podman run dns", func() { session := podmanTest.Podman([]string{"run", "--dns=1.2.3.4", ALPINE, "cat", "/etc/resolv.conf"}) session.WaitWithDefaultTimeout() Expect(session.ExitCode()).To(Equal(0)) - session.LineInOuputStartsWith("server 1.2.3.4") + session.LineInOutputStartsWith("server 1.2.3.4") }) It("podman run add dns option", func() { session := podmanTest.Podman([]string{"run", "--dns-opt=debug", ALPINE, "cat", "/etc/resolv.conf"}) session.WaitWithDefaultTimeout() Expect(session.ExitCode()).To(Equal(0)) - session.LineInOuputStartsWith("options debug") + session.LineInOutputStartsWith("options debug") }) It("podman run add bad host", func() { @@ -76,8 +76,8 @@ var _ = Describe("Podman run dns", func() { session := podmanTest.Podman([]string{"run", "--add-host=foobar:1.1.1.1", "--add-host=foobaz:2001:db8::68", ALPINE, "cat", "/etc/hosts"}) session.WaitWithDefaultTimeout() Expect(session.ExitCode()).To(Equal(0)) - session.LineInOuputStartsWith("1.1.1.1 foobar") - session.LineInOuputStartsWith("2001:db8::68 foobaz") + session.LineInOutputStartsWith("1.1.1.1 foobar") + session.LineInOutputStartsWith("2001:db8::68 foobaz") }) It("podman run add hostname", func() { diff --git a/test/e2e/run_entrypoint_test.go b/test/e2e/run_entrypoint_test.go index 2185d6b13..cac3d759d 100644 --- a/test/e2e/run_entrypoint_test.go +++ b/test/e2e/run_entrypoint_test.go @@ -99,12 +99,12 @@ ENTRYPOINT ["grep", "Alpine", "/etc/os-release"] session := podmanTest.Podman([]string{"run", "--entrypoint=uname", "foobar.com/entrypoint:latest"}) session.WaitWithDefaultTimeout() Expect(session.ExitCode()).To(Equal(0)) - Expect(session.LineInOuputStartsWith("Linux")).To(BeTrue()) + Expect(session.LineInOutputStartsWith("Linux")).To(BeTrue()) session = podmanTest.Podman([]string{"run", "--entrypoint", "", "foobar.com/entrypoint:latest", "uname"}) session.WaitWithDefaultTimeout() Expect(session.ExitCode()).To(Equal(0)) - Expect(session.LineInOuputStartsWith("Linux")).To(BeTrue()) + Expect(session.LineInOutputStartsWith("Linux")).To(BeTrue()) }) It("podman run user entrypoint with command overrides image entrypoint and image cmd", func() { @@ -116,6 +116,6 @@ ENTRYPOINT ["grep", "Alpine", "/etc/os-release"] session := podmanTest.Podman([]string{"run", "--entrypoint=uname", "foobar.com/entrypoint:latest", "-r"}) session.WaitWithDefaultTimeout() Expect(session.ExitCode()).To(Equal(0)) - Expect(session.LineInOuputStartsWith("Linux")).To(BeFalse()) + Expect(session.LineInOutputStartsWith("Linux")).To(BeFalse()) }) }) diff --git a/test/system/010-images.bats b/test/system/010-images.bats index 98bb0cc57..76caf282b 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 @@ -197,9 +199,16 @@ Labels.created_at | 20[0-9-]\\\+T[0-9:]\\\+Z local format=$2 run_podman images --sort repository --format "$format" - _check_line 0 ${aaa_name} ${aaa_tag} - _check_line 1 "${PODMAN_TEST_IMAGE_REGISTRY}/${PODMAN_TEST_IMAGE_USER}/${PODMAN_TEST_IMAGE_NAME}" "${PODMAN_TEST_IMAGE_TAG}" - _check_line 2 ${zzz_name} ${zzz_tag} + + line_no=0 + if [[ $format == table* ]]; then + # skip headers from table command + line_no=1 + fi + + _check_line $line_no ${aaa_name} ${aaa_tag} + _check_line $((line_no+1)) "${PODMAN_TEST_IMAGE_REGISTRY}/${PODMAN_TEST_IMAGE_USER}/${PODMAN_TEST_IMAGE_NAME}" "${PODMAN_TEST_IMAGE_TAG}" + _check_line $((line_no+2)) ${zzz_name} ${zzz_tag} } # Begin the test: tag $IMAGE with both the given names 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/test/utils/podmansession_test.go b/test/utils/podmansession_test.go index d0e2f3d06..763cb4f26 100644 --- a/test/utils/podmansession_test.go +++ b/test/utils/podmansession_test.go @@ -53,8 +53,8 @@ var _ = Describe("PodmanSession test", func() { }) It("Test LineInOutputStartsWith", func() { - Expect(session.LineInOuputStartsWith("Podman")).To(BeTrue()) - Expect(session.LineInOuputStartsWith("Session")).To(Not(BeTrue())) + Expect(session.LineInOutputStartsWith("Podman")).To(BeTrue()) + Expect(session.LineInOutputStartsWith("Session")).To(Not(BeTrue())) }) It("Test LineInOutputContains", func() { diff --git a/test/utils/utils.go b/test/utils/utils.go index dd836f258..d08939678 100644 --- a/test/utils/utils.go +++ b/test/utils/utils.go @@ -277,7 +277,7 @@ func (s *PodmanSession) ErrorGrepString(term string) (bool, []string) { // LineInOutputStartsWith returns true if a line in a // session output starts with the supplied string -func (s *PodmanSession) LineInOuputStartsWith(term string) bool { +func (s *PodmanSession) LineInOutputStartsWith(term string) bool { for _, i := range s.OutputToStringArray() { if strings.HasPrefix(i, term) { return true diff --git a/vendor/github.com/coreos/go-systemd/LICENSE b/vendor/github.com/coreos/go-systemd/LICENSE new file mode 100644 index 000000000..37ec93a14 --- /dev/null +++ b/vendor/github.com/coreos/go-systemd/LICENSE @@ -0,0 +1,191 @@ +Apache License +Version 2.0, January 2004 +http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + +"License" shall mean the terms and conditions for use, reproduction, and +distribution as defined by Sections 1 through 9 of this document. + +"Licensor" shall mean the copyright owner or entity authorized by the copyright +owner that is granting the License. + +"Legal Entity" shall mean the union of the acting entity and all other entities +that control, are controlled by, or are under common control with that entity. +For the purposes of this definition, "control" means (i) the power, direct or +indirect, to cause the direction or management of such entity, whether by +contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the +outstanding shares, or (iii) beneficial ownership of such entity. + +"You" (or "Your") shall mean an individual or Legal Entity exercising +permissions granted by this License. + +"Source" form shall mean the preferred form for making modifications, including +but not limited to software source code, documentation source, and configuration +files. + +"Object" form shall mean any form resulting from mechanical transformation or +translation of a Source form, including but not limited to compiled object code, +generated documentation, and conversions to other media types. + +"Work" shall mean the work of authorship, whether in Source or Object form, made +available under the License, as indicated by a copyright notice that is included +in or attached to the work (an example is provided in the Appendix below). + +"Derivative Works" shall mean any work, whether in Source or Object form, that +is based on (or derived from) the Work and for which the editorial revisions, +annotations, elaborations, or other modifications represent, as a whole, an +original work of authorship. For the purposes of this License, Derivative Works +shall not include works that remain separable from, or merely link (or bind by +name) to the interfaces of, the Work and Derivative Works thereof. + +"Contribution" shall mean any work of authorship, including the original version +of the Work and any modifications or additions to that Work or Derivative Works +thereof, that is intentionally submitted to Licensor for inclusion in the Work +by the copyright owner or by an individual or Legal Entity authorized to submit +on behalf of the copyright owner. For the purposes of this definition, +"submitted" means any form of electronic, verbal, or written communication sent +to the Licensor or its representatives, including but not limited to +communication on electronic mailing lists, source code control systems, and +issue tracking systems that are managed by, or on behalf of, the Licensor for +the purpose of discussing and improving the Work, but excluding communication +that is conspicuously marked or otherwise designated in writing by the copyright +owner as "Not a Contribution." + +"Contributor" shall mean Licensor and any individual or Legal Entity on behalf +of whom a Contribution has been received by Licensor and subsequently +incorporated within the Work. + +2. Grant of Copyright License. + +Subject to the terms and conditions of this License, each Contributor hereby +grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, +irrevocable copyright license to reproduce, prepare Derivative Works of, +publicly display, publicly perform, sublicense, and distribute the Work and such +Derivative Works in Source or Object form. + +3. Grant of Patent License. + +Subject to the terms and conditions of this License, each Contributor hereby +grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, +irrevocable (except as stated in this section) patent license to make, have +made, use, offer to sell, sell, import, and otherwise transfer the Work, where +such license applies only to those patent claims licensable by such Contributor +that are necessarily infringed by their Contribution(s) alone or by combination +of their Contribution(s) with the Work to which such Contribution(s) was +submitted. If You institute patent litigation against any entity (including a +cross-claim or counterclaim in a lawsuit) alleging that the Work or a +Contribution incorporated within the Work constitutes direct or contributory +patent infringement, then any patent licenses granted to You under this License +for that Work shall terminate as of the date such litigation is filed. + +4. Redistribution. + +You may reproduce and distribute copies of the Work or Derivative Works thereof +in any medium, with or without modifications, and in Source or Object form, +provided that You meet the following conditions: + +You must give any other recipients of the Work or Derivative Works a copy of +this License; and +You must cause any modified files to carry prominent notices stating that You +changed the files; and +You must retain, in the Source form of any Derivative Works that You distribute, +all copyright, patent, trademark, and attribution notices from the Source form +of the Work, excluding those notices that do not pertain to any part of the +Derivative Works; and +If the Work includes a "NOTICE" text file as part of its distribution, then any +Derivative Works that You distribute must include a readable copy of the +attribution notices contained within such NOTICE file, excluding those notices +that do not pertain to any part of the Derivative Works, in at least one of the +following places: within a NOTICE text file distributed as part of the +Derivative Works; within the Source form or documentation, if provided along +with the Derivative Works; or, within a display generated by the Derivative +Works, if and wherever such third-party notices normally appear. The contents of +the NOTICE file are for informational purposes only and do not modify the +License. You may add Your own attribution notices within Derivative Works that +You distribute, alongside or as an addendum to the NOTICE text from the Work, +provided that such additional attribution notices cannot be construed as +modifying the License. +You may add Your own copyright statement to Your modifications and may provide +additional or different license terms and conditions for use, reproduction, or +distribution of Your modifications, or for any such Derivative Works as a whole, +provided Your use, reproduction, and distribution of the Work otherwise complies +with the conditions stated in this License. + +5. Submission of Contributions. + +Unless You explicitly state otherwise, any Contribution intentionally submitted +for inclusion in the Work by You to the Licensor shall be under the terms and +conditions of this License, without any additional terms or conditions. +Notwithstanding the above, nothing herein shall supersede or modify the terms of +any separate license agreement you may have executed with Licensor regarding +such Contributions. + +6. Trademarks. + +This License does not grant permission to use the trade names, trademarks, +service marks, or product names of the Licensor, except as required for +reasonable and customary use in describing the origin of the Work and +reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. + +Unless required by applicable law or agreed to in writing, Licensor provides the +Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, +including, without limitation, any warranties or conditions of TITLE, +NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are +solely responsible for determining the appropriateness of using or +redistributing the Work and assume any risks associated with Your exercise of +permissions under this License. + +8. Limitation of Liability. + +In no event and under no legal theory, whether in tort (including negligence), +contract, or otherwise, unless required by applicable law (such as deliberate +and grossly negligent acts) or agreed to in writing, shall any Contributor be +liable to You for damages, including any direct, indirect, special, incidental, +or consequential damages of any character arising as a result of this License or +out of the use or inability to use the Work (including but not limited to +damages for loss of goodwill, work stoppage, computer failure or malfunction, or +any and all other commercial damages or losses), even if such Contributor has +been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. + +While redistributing the Work or Derivative Works thereof, You may choose to +offer, and charge a fee for, acceptance of support, warranty, indemnity, or +other liability obligations and/or rights consistent with this License. However, +in accepting such obligations, You may act only on Your own behalf and on Your +sole responsibility, not on behalf of any other Contributor, and only if You +agree to indemnify, defend, and hold each Contributor harmless for any liability +incurred by, or claims asserted against, such Contributor by reason of your +accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work + +To apply the Apache License to your work, attach the following boilerplate +notice, with the fields enclosed by brackets "[]" replaced with your own +identifying information. (Don't include the brackets!) The text should be +enclosed in the appropriate comment syntax for the file format. We also +recommend that a file or class name and description of purpose be included on +the same "printed page" as the copyright notice for easier identification within +third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/vendor/github.com/coreos/go-systemd/NOTICE b/vendor/github.com/coreos/go-systemd/NOTICE new file mode 100644 index 000000000..23a0ada2f --- /dev/null +++ b/vendor/github.com/coreos/go-systemd/NOTICE @@ -0,0 +1,5 @@ +CoreOS Project +Copyright 2018 CoreOS, Inc + +This product includes software developed at CoreOS, Inc. +(http://www.coreos.com/). diff --git a/vendor/github.com/coreos/go-systemd/activation/files.go b/vendor/github.com/coreos/go-systemd/activation/files.go new file mode 100644 index 000000000..29dd18def --- /dev/null +++ b/vendor/github.com/coreos/go-systemd/activation/files.go @@ -0,0 +1,67 @@ +// Copyright 2015 CoreOS, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package activation implements primitives for systemd socket activation. +package activation + +import ( + "os" + "strconv" + "strings" + "syscall" +) + +const ( + // listenFdsStart corresponds to `SD_LISTEN_FDS_START`. + listenFdsStart = 3 +) + +// Files returns a slice containing a `os.File` object for each +// file descriptor passed to this process via systemd fd-passing protocol. +// +// The order of the file descriptors is preserved in the returned slice. +// `unsetEnv` is typically set to `true` in order to avoid clashes in +// fd usage and to avoid leaking environment flags to child processes. +func Files(unsetEnv bool) []*os.File { + if unsetEnv { + defer os.Unsetenv("LISTEN_PID") + defer os.Unsetenv("LISTEN_FDS") + defer os.Unsetenv("LISTEN_FDNAMES") + } + + pid, err := strconv.Atoi(os.Getenv("LISTEN_PID")) + if err != nil || pid != os.Getpid() { + return nil + } + + nfds, err := strconv.Atoi(os.Getenv("LISTEN_FDS")) + if err != nil || nfds == 0 { + return nil + } + + names := strings.Split(os.Getenv("LISTEN_FDNAMES"), ":") + + files := make([]*os.File, 0, nfds) + for fd := listenFdsStart; fd < listenFdsStart+nfds; fd++ { + syscall.CloseOnExec(fd) + name := "LISTEN_FD_" + strconv.Itoa(fd) + offset := fd - listenFdsStart + if offset < len(names) && len(names[offset]) > 0 { + name = names[offset] + } + files = append(files, os.NewFile(uintptr(fd), name)) + } + + return files +} diff --git a/vendor/github.com/coreos/go-systemd/activation/listeners.go b/vendor/github.com/coreos/go-systemd/activation/listeners.go new file mode 100644 index 000000000..bb5cc2311 --- /dev/null +++ b/vendor/github.com/coreos/go-systemd/activation/listeners.go @@ -0,0 +1,103 @@ +// Copyright 2015 CoreOS, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package activation + +import ( + "crypto/tls" + "net" +) + +// Listeners returns a slice containing a net.Listener for each matching socket type +// passed to this process. +// +// The order of the file descriptors is preserved in the returned slice. +// Nil values are used to fill any gaps. For example if systemd were to return file descriptors +// corresponding with "udp, tcp, tcp", then the slice would contain {nil, net.Listener, net.Listener} +func Listeners() ([]net.Listener, error) { + files := Files(true) + listeners := make([]net.Listener, len(files)) + + for i, f := range files { + if pc, err := net.FileListener(f); err == nil { + listeners[i] = pc + f.Close() + } + } + return listeners, nil +} + +// ListenersWithNames maps a listener name to a set of net.Listener instances. +func ListenersWithNames() (map[string][]net.Listener, error) { + files := Files(true) + listeners := map[string][]net.Listener{} + + for _, f := range files { + if pc, err := net.FileListener(f); err == nil { + current, ok := listeners[f.Name()] + if !ok { + listeners[f.Name()] = []net.Listener{pc} + } else { + listeners[f.Name()] = append(current, pc) + } + f.Close() + } + } + return listeners, nil +} + +// TLSListeners returns a slice containing a net.listener for each matching TCP socket type +// passed to this process. +// It uses default Listeners func and forces TCP sockets handlers to use TLS based on tlsConfig. +func TLSListeners(tlsConfig *tls.Config) ([]net.Listener, error) { + listeners, err := Listeners() + + if listeners == nil || err != nil { + return nil, err + } + + if tlsConfig != nil && err == nil { + for i, l := range listeners { + // Activate TLS only for TCP sockets + if l.Addr().Network() == "tcp" { + listeners[i] = tls.NewListener(l, tlsConfig) + } + } + } + + return listeners, err +} + +// TLSListenersWithNames maps a listener name to a net.Listener with +// the associated TLS configuration. +func TLSListenersWithNames(tlsConfig *tls.Config) (map[string][]net.Listener, error) { + listeners, err := ListenersWithNames() + + if listeners == nil || err != nil { + return nil, err + } + + if tlsConfig != nil && err == nil { + for _, ll := range listeners { + // Activate TLS only for TCP sockets + for i, l := range ll { + if l.Addr().Network() == "tcp" { + ll[i] = tls.NewListener(l, tlsConfig) + } + } + } + } + + return listeners, err +} diff --git a/vendor/github.com/coreos/go-systemd/activation/packetconns.go b/vendor/github.com/coreos/go-systemd/activation/packetconns.go new file mode 100644 index 000000000..a97206785 --- /dev/null +++ b/vendor/github.com/coreos/go-systemd/activation/packetconns.go @@ -0,0 +1,38 @@ +// Copyright 2015 CoreOS, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package activation + +import ( + "net" +) + +// PacketConns returns a slice containing a net.PacketConn for each matching socket type +// passed to this process. +// +// The order of the file descriptors is preserved in the returned slice. +// Nil values are used to fill any gaps. For example if systemd were to return file descriptors +// corresponding with "udp, tcp, udp", then the slice would contain {net.PacketConn, nil, net.PacketConn} +func PacketConns() ([]net.PacketConn, error) { + files := Files(true) + conns := make([]net.PacketConn, len(files)) + + for i, f := range files { + if pc, err := net.FilePacketConn(f); err == nil { + conns[i] = pc + f.Close() + } + } + return conns, nil +} diff --git a/vendor/github.com/docker/go-plugins-helpers/LICENSE b/vendor/github.com/docker/go-plugins-helpers/LICENSE new file mode 100644 index 000000000..8f71f43fe --- /dev/null +++ b/vendor/github.com/docker/go-plugins-helpers/LICENSE @@ -0,0 +1,202 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright {yyyy} {name of copyright owner} + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + diff --git a/vendor/github.com/docker/go-plugins-helpers/NOTICE b/vendor/github.com/docker/go-plugins-helpers/NOTICE new file mode 100644 index 000000000..6e6f469ab --- /dev/null +++ b/vendor/github.com/docker/go-plugins-helpers/NOTICE @@ -0,0 +1,19 @@ +Docker +Copyright 2012-2015 Docker, Inc. + +This product includes software developed at Docker, Inc. (https://www.docker.com). + +This product contains software (https://github.com/kr/pty) developed +by Keith Rarick, licensed under the MIT License. + +The following is courtesy of our legal counsel: + + +Use and transfer of Docker may be subject to certain restrictions by the +United States and other governments. +It is your responsibility to ensure that your use and/or transfer does not +violate applicable laws. + +For more information, please see https://www.bis.doc.gov + +See also https://www.apache.org/dev/crypto.html and/or seek legal counsel. diff --git a/vendor/github.com/docker/go-plugins-helpers/sdk/encoder.go b/vendor/github.com/docker/go-plugins-helpers/sdk/encoder.go new file mode 100644 index 000000000..195812a44 --- /dev/null +++ b/vendor/github.com/docker/go-plugins-helpers/sdk/encoder.go @@ -0,0 +1,37 @@ +package sdk + +import ( + "encoding/json" + "fmt" + "io" + "net/http" +) + +// DefaultContentTypeV1_1 is the default content type accepted and sent by the plugins. +const DefaultContentTypeV1_1 = "application/vnd.docker.plugins.v1.1+json" + +// DecodeRequest decodes an http request into a given structure. +func DecodeRequest(w http.ResponseWriter, r *http.Request, req interface{}) (err error) { + if err = json.NewDecoder(r.Body).Decode(req); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + } + return +} + +// EncodeResponse encodes the given structure into an http response. +func EncodeResponse(w http.ResponseWriter, res interface{}, err bool) { + w.Header().Set("Content-Type", DefaultContentTypeV1_1) + if err { + w.WriteHeader(http.StatusInternalServerError) + } + json.NewEncoder(w).Encode(res) +} + +// StreamResponse streams a response object to the client +func StreamResponse(w http.ResponseWriter, data io.ReadCloser) { + w.Header().Set("Content-Type", DefaultContentTypeV1_1) + if _, err := copyBuf(w, data); err != nil { + fmt.Printf("ERROR in stream: %v\n", err) + } + data.Close() +} diff --git a/vendor/github.com/docker/go-plugins-helpers/sdk/handler.go b/vendor/github.com/docker/go-plugins-helpers/sdk/handler.go new file mode 100644 index 000000000..c0d042ed0 --- /dev/null +++ b/vendor/github.com/docker/go-plugins-helpers/sdk/handler.go @@ -0,0 +1,88 @@ +package sdk + +import ( + "crypto/tls" + "fmt" + "net" + "net/http" + "os" +) + +const activatePath = "/Plugin.Activate" + +// Handler is the base to create plugin handlers. +// It initializes connections and sockets to listen to. +type Handler struct { + mux *http.ServeMux +} + +// NewHandler creates a new Handler with an http mux. +func NewHandler(manifest string) Handler { + mux := http.NewServeMux() + + mux.HandleFunc(activatePath, func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", DefaultContentTypeV1_1) + fmt.Fprintln(w, manifest) + }) + + return Handler{mux: mux} +} + +// Serve sets up the handler to serve requests on the passed in listener +func (h Handler) Serve(l net.Listener) error { + server := http.Server{ + Addr: l.Addr().String(), + Handler: h.mux, + } + return server.Serve(l) +} + +// ServeTCP makes the handler to listen for request in a given TCP address. +// It also writes the spec file in the right directory for docker to read. +// Due to constrains for running Docker in Docker on Windows, data-root directory +// of docker daemon must be provided. To get default directory, use +// WindowsDefaultDaemonRootDir() function. On Unix, this parameter is ignored. +func (h Handler) ServeTCP(pluginName, addr, daemonDir string, tlsConfig *tls.Config) error { + l, spec, err := newTCPListener(addr, pluginName, daemonDir, tlsConfig) + if err != nil { + return err + } + if spec != "" { + defer os.Remove(spec) + } + return h.Serve(l) +} + +// ServeUnix makes the handler to listen for requests in a unix socket. +// It also creates the socket file in the right directory for docker to read. +func (h Handler) ServeUnix(addr string, gid int) error { + l, spec, err := newUnixListener(addr, gid) + if err != nil { + return err + } + if spec != "" { + defer os.Remove(spec) + } + return h.Serve(l) +} + +// ServeWindows makes the handler to listen for request in a Windows named pipe. +// It also creates the spec file in the right directory for docker to read. +// Due to constrains for running Docker in Docker on Windows, data-root directory +// of docker daemon must be provided. To get default directory, use +// WindowsDefaultDaemonRootDir() function. On Unix, this parameter is ignored. +func (h Handler) ServeWindows(addr, pluginName, daemonDir string, pipeConfig *WindowsPipeConfig) error { + l, spec, err := newWindowsListener(addr, pluginName, daemonDir, pipeConfig) + if err != nil { + return err + } + if spec != "" { + defer os.Remove(spec) + } + return h.Serve(l) +} + +// HandleFunc registers a function to handle a request path with. +func (h Handler) HandleFunc(path string, fn func(w http.ResponseWriter, r *http.Request)) { + h.mux.HandleFunc(path, fn) +} diff --git a/vendor/github.com/docker/go-plugins-helpers/sdk/pool.go b/vendor/github.com/docker/go-plugins-helpers/sdk/pool.go new file mode 100644 index 000000000..316775973 --- /dev/null +++ b/vendor/github.com/docker/go-plugins-helpers/sdk/pool.go @@ -0,0 +1,18 @@ +package sdk + +import ( + "io" + "sync" +) + +const buffer32K = 32 * 1024 + +var buffer32KPool = &sync.Pool{New: func() interface{} { return make([]byte, buffer32K) }} + +// copyBuf uses a shared buffer pool with io.CopyBuffer +func copyBuf(w io.Writer, r io.Reader) (int64, error) { + buf := buffer32KPool.Get().([]byte) + written, err := io.CopyBuffer(w, r, buf) + buffer32KPool.Put(buf) + return written, err +} diff --git a/vendor/github.com/docker/go-plugins-helpers/sdk/spec_file_generator.go b/vendor/github.com/docker/go-plugins-helpers/sdk/spec_file_generator.go new file mode 100644 index 000000000..bc8cfc644 --- /dev/null +++ b/vendor/github.com/docker/go-plugins-helpers/sdk/spec_file_generator.go @@ -0,0 +1,58 @@ +package sdk + +import ( + "fmt" + "io/ioutil" + "os" + "path/filepath" +) + +type protocol string + +const ( + protoTCP protocol = "tcp" + protoNamedPipe protocol = "npipe" +) + +// PluginSpecDir returns plugin spec dir in relation to daemon root directory. +func PluginSpecDir(daemonRoot string) string { + return ([]string{filepath.Join(daemonRoot, "plugins")})[0] +} + +// WindowsDefaultDaemonRootDir returns default data directory of docker daemon on Windows. +func WindowsDefaultDaemonRootDir() string { + return filepath.Join(os.Getenv("programdata"), "docker") +} + +func createPluginSpecDirWindows(name, address, daemonRoot string) (string, error) { + _, err := os.Stat(daemonRoot) + if os.IsNotExist(err) { + return "", fmt.Errorf("Deamon root directory must already exist: %s", err) + } + + pluginSpecDir := PluginSpecDir(daemonRoot) + + if err := windowsCreateDirectoryWithACL(pluginSpecDir); err != nil { + return "", err + } + return pluginSpecDir, nil +} + +func createPluginSpecDirUnix(name, address string) (string, error) { + pluginSpecDir := PluginSpecDir("/etc/docker") + if err := os.MkdirAll(pluginSpecDir, 0755); err != nil { + return "", err + } + return pluginSpecDir, nil +} + +func writeSpecFile(name, address, pluginSpecDir string, proto protocol) (string, error) { + specFileDir := filepath.Join(pluginSpecDir, name+".spec") + + url := string(proto) + "://" + address + if err := ioutil.WriteFile(specFileDir, []byte(url), 0644); err != nil { + return "", err + } + + return specFileDir, nil +} diff --git a/vendor/github.com/docker/go-plugins-helpers/sdk/tcp_listener.go b/vendor/github.com/docker/go-plugins-helpers/sdk/tcp_listener.go new file mode 100644 index 000000000..bad85f7fd --- /dev/null +++ b/vendor/github.com/docker/go-plugins-helpers/sdk/tcp_listener.go @@ -0,0 +1,34 @@ +package sdk + +import ( + "crypto/tls" + "net" + "runtime" + + "github.com/docker/go-connections/sockets" +) + +func newTCPListener(address, pluginName, daemonDir string, tlsConfig *tls.Config) (net.Listener, string, error) { + listener, err := sockets.NewTCPSocket(address, tlsConfig) + if err != nil { + return nil, "", err + } + + addr := listener.Addr().String() + + var specDir string + if runtime.GOOS == "windows" { + specDir, err = createPluginSpecDirWindows(pluginName, addr, daemonDir) + } else { + specDir, err = createPluginSpecDirUnix(pluginName, addr) + } + if err != nil { + return nil, "", err + } + + specFile, err := writeSpecFile(pluginName, addr, specDir, protoTCP) + if err != nil { + return nil, "", err + } + return listener, specFile, nil +} diff --git a/vendor/github.com/docker/go-plugins-helpers/sdk/unix_listener.go b/vendor/github.com/docker/go-plugins-helpers/sdk/unix_listener.go new file mode 100644 index 000000000..54b9a6d31 --- /dev/null +++ b/vendor/github.com/docker/go-plugins-helpers/sdk/unix_listener.go @@ -0,0 +1,35 @@ +// +build linux freebsd + +package sdk + +import ( + "net" + "os" + "path/filepath" + + "github.com/docker/go-connections/sockets" +) + +const pluginSockDir = "/run/docker/plugins" + +func newUnixListener(pluginName string, gid int) (net.Listener, string, error) { + path, err := fullSocketAddress(pluginName) + if err != nil { + return nil, "", err + } + listener, err := sockets.NewUnixSocket(path, gid) + if err != nil { + return nil, "", err + } + return listener, path, nil +} + +func fullSocketAddress(address string) (string, error) { + if err := os.MkdirAll(pluginSockDir, 0755); err != nil { + return "", err + } + if filepath.IsAbs(address) { + return address, nil + } + return filepath.Join(pluginSockDir, address+".sock"), nil +} diff --git a/vendor/github.com/docker/go-plugins-helpers/sdk/unix_listener_nosystemd.go b/vendor/github.com/docker/go-plugins-helpers/sdk/unix_listener_nosystemd.go new file mode 100644 index 000000000..a798b8722 --- /dev/null +++ b/vendor/github.com/docker/go-plugins-helpers/sdk/unix_listener_nosystemd.go @@ -0,0 +1,10 @@ +// +build linux freebsd +// +build nosystemd + +package sdk + +import "net" + +func setupSocketActivation() (net.Listener, error) { + return nil, nil +} diff --git a/vendor/github.com/docker/go-plugins-helpers/sdk/unix_listener_systemd.go b/vendor/github.com/docker/go-plugins-helpers/sdk/unix_listener_systemd.go new file mode 100644 index 000000000..5d5d8f427 --- /dev/null +++ b/vendor/github.com/docker/go-plugins-helpers/sdk/unix_listener_systemd.go @@ -0,0 +1,45 @@ +// +build linux freebsd +// +build !nosystemd + +package sdk + +import ( + "fmt" + "net" + "os" + + "github.com/coreos/go-systemd/activation" +) + +// isRunningSystemd checks whether the host was booted with systemd as its init +// system. This functions similarly to systemd's `sd_booted(3)`: internally, it +// checks whether /run/systemd/system/ exists and is a directory. +// http://www.freedesktop.org/software/systemd/man/sd_booted.html +// +// Copied from github.com/coreos/go-systemd/util.IsRunningSystemd +func isRunningSystemd() bool { + fi, err := os.Lstat("/run/systemd/system") + if err != nil { + return false + } + return fi.IsDir() +} + +func setupSocketActivation() (net.Listener, error) { + if !isRunningSystemd() { + return nil, nil + } + listenFds := activation.Files(false) + if len(listenFds) > 1 { + return nil, fmt.Errorf("expected only one socket from systemd, got %d", len(listenFds)) + } + var listener net.Listener + if len(listenFds) == 1 { + l, err := net.FileListener(listenFds[0]) + if err != nil { + return nil, err + } + listener = l + } + return listener, nil +} diff --git a/vendor/github.com/docker/go-plugins-helpers/sdk/unix_listener_unsupported.go b/vendor/github.com/docker/go-plugins-helpers/sdk/unix_listener_unsupported.go new file mode 100644 index 000000000..344cf751b --- /dev/null +++ b/vendor/github.com/docker/go-plugins-helpers/sdk/unix_listener_unsupported.go @@ -0,0 +1,16 @@ +// +build !linux,!freebsd + +package sdk + +import ( + "errors" + "net" +) + +var ( + errOnlySupportedOnLinuxAndFreeBSD = errors.New("unix socket creation is only supported on Linux and FreeBSD") +) + +func newUnixListener(pluginName string, gid int) (net.Listener, string, error) { + return nil, "", errOnlySupportedOnLinuxAndFreeBSD +} diff --git a/vendor/github.com/docker/go-plugins-helpers/sdk/windows_listener.go b/vendor/github.com/docker/go-plugins-helpers/sdk/windows_listener.go new file mode 100644 index 000000000..b5deaba6d --- /dev/null +++ b/vendor/github.com/docker/go-plugins-helpers/sdk/windows_listener.go @@ -0,0 +1,70 @@ +// +build windows + +package sdk + +import ( + "net" + "os" + "syscall" + "unsafe" + + "github.com/Microsoft/go-winio" +) + +// Named pipes use Windows Security Descriptor Definition Language to define ACL. Following are +// some useful definitions. +const ( + // This will set permissions for everyone to have full access + AllowEveryone = "S:(ML;;NW;;;LW)D:(A;;0x12019f;;;WD)" + + // This will set permissions for Service, System, Adminstrator group and account to have full access + AllowServiceSystemAdmin = "D:(A;ID;FA;;;SY)(A;ID;FA;;;BA)(A;ID;FA;;;LA)(A;ID;FA;;;LS)" +) + +func newWindowsListener(address, pluginName, daemonRoot string, pipeConfig *WindowsPipeConfig) (net.Listener, string, error) { + winioPipeConfig := winio.PipeConfig{ + SecurityDescriptor: pipeConfig.SecurityDescriptor, + InputBufferSize: pipeConfig.InBufferSize, + OutputBufferSize: pipeConfig.OutBufferSize, + } + listener, err := winio.ListenPipe(address, &winioPipeConfig) + if err != nil { + return nil, "", err + } + + addr := listener.Addr().String() + + specDir, err := createPluginSpecDirWindows(pluginName, addr, daemonRoot) + if err != nil { + return nil, "", err + } + + spec, err := writeSpecFile(pluginName, addr, specDir, protoNamedPipe) + if err != nil { + return nil, "", err + } + return listener, spec, nil +} + +func windowsCreateDirectoryWithACL(name string) error { + sa := syscall.SecurityAttributes{Length: 0} + sddl := "D:P(A;OICI;GA;;;BA)(A;OICI;GA;;;SY)" + sd, err := winio.SddlToSecurityDescriptor(sddl) + if err != nil { + return &os.PathError{Op: "mkdir", Path: name, Err: err} + } + sa.Length = uint32(unsafe.Sizeof(sa)) + sa.InheritHandle = 1 + sa.SecurityDescriptor = uintptr(unsafe.Pointer(&sd[0])) + + namep, err := syscall.UTF16PtrFromString(name) + if err != nil { + return &os.PathError{Op: "mkdir", Path: name, Err: err} + } + + e := syscall.CreateDirectory(namep, &sa) + if e != nil { + return &os.PathError{Op: "mkdir", Path: name, Err: e} + } + return nil +} diff --git a/vendor/github.com/docker/go-plugins-helpers/sdk/windows_listener_unsupported.go b/vendor/github.com/docker/go-plugins-helpers/sdk/windows_listener_unsupported.go new file mode 100644 index 000000000..0f5e113c1 --- /dev/null +++ b/vendor/github.com/docker/go-plugins-helpers/sdk/windows_listener_unsupported.go @@ -0,0 +1,20 @@ +// +build !windows + +package sdk + +import ( + "errors" + "net" +) + +var ( + errOnlySupportedOnWindows = errors.New("named pipe creation is only supported on Windows") +) + +func newWindowsListener(address, pluginName, daemonRoot string, pipeConfig *WindowsPipeConfig) (net.Listener, string, error) { + return nil, "", errOnlySupportedOnWindows +} + +func windowsCreateDirectoryWithACL(name string) error { + return nil +} diff --git a/vendor/github.com/docker/go-plugins-helpers/sdk/windows_pipe_config.go b/vendor/github.com/docker/go-plugins-helpers/sdk/windows_pipe_config.go new file mode 100644 index 000000000..256fa3d67 --- /dev/null +++ b/vendor/github.com/docker/go-plugins-helpers/sdk/windows_pipe_config.go @@ -0,0 +1,13 @@ +package sdk + +// WindowsPipeConfig is a helper structure for configuring named pipe parameters on Windows. +type WindowsPipeConfig struct { + // SecurityDescriptor contains a Windows security descriptor in SDDL format. + SecurityDescriptor string + + // InBufferSize in bytes. + InBufferSize int32 + + // OutBufferSize in bytes. + OutBufferSize int32 +} diff --git a/vendor/github.com/docker/go-plugins-helpers/volume/README.md b/vendor/github.com/docker/go-plugins-helpers/volume/README.md new file mode 100644 index 000000000..395aa643f --- /dev/null +++ b/vendor/github.com/docker/go-plugins-helpers/volume/README.md @@ -0,0 +1,36 @@ +# Docker volume extension api. + +Go handler to create external volume extensions for Docker. + +## Usage + +This library is designed to be integrated in your program. + +1. Implement the `volume.Driver` interface. +2. Initialize a `volume.Handler` with your implementation. +3. Call either `ServeTCP` or `ServeUnix` from the `volume.Handler`. + +### Example using TCP sockets: + +```go + d := MyVolumeDriver{} + h := volume.NewHandler(d) + h.ServeTCP("test_volume", ":8080") +``` + +### Example using Unix sockets: + +```go + d := MyVolumeDriver{} + h := volume.NewHandler(d) + u, _ := user.Lookup("root") + gid, _ := strconv.Atoi(u.Gid) + h.ServeUnix("test_volume", gid) +``` + +## Full example plugins + +- https://github.com/calavera/docker-volume-glusterfs +- https://github.com/calavera/docker-volume-keywhiz +- https://github.com/quobyte/docker-volume +- https://github.com/NimbleStorage/Nemo diff --git a/vendor/github.com/docker/go-plugins-helpers/volume/api.go b/vendor/github.com/docker/go-plugins-helpers/volume/api.go new file mode 100644 index 000000000..dcc2f3abf --- /dev/null +++ b/vendor/github.com/docker/go-plugins-helpers/volume/api.go @@ -0,0 +1,230 @@ +package volume + +import ( + "log" + "net/http" + + "github.com/docker/go-plugins-helpers/sdk" +) + +const ( + // DefaultDockerRootDirectory is the default directory where volumes will be created. + DefaultDockerRootDirectory = "/var/lib/docker-volumes" + + manifest = `{"Implements": ["VolumeDriver"]}` + createPath = "/VolumeDriver.Create" + getPath = "/VolumeDriver.Get" + listPath = "/VolumeDriver.List" + removePath = "/VolumeDriver.Remove" + hostVirtualPath = "/VolumeDriver.Path" + mountPath = "/VolumeDriver.Mount" + unmountPath = "/VolumeDriver.Unmount" + capabilitiesPath = "/VolumeDriver.Capabilities" +) + +// CreateRequest is the structure that docker's requests are deserialized to. +type CreateRequest struct { + Name string + Options map[string]string `json:"Opts,omitempty"` +} + +// RemoveRequest structure for a volume remove request +type RemoveRequest struct { + Name string +} + +// MountRequest structure for a volume mount request +type MountRequest struct { + Name string + ID string +} + +// MountResponse structure for a volume mount response +type MountResponse struct { + Mountpoint string +} + +// UnmountRequest structure for a volume unmount request +type UnmountRequest struct { + Name string + ID string +} + +// PathRequest structure for a volume path request +type PathRequest struct { + Name string +} + +// PathResponse structure for a volume path response +type PathResponse struct { + Mountpoint string +} + +// GetRequest structure for a volume get request +type GetRequest struct { + Name string +} + +// GetResponse structure for a volume get response +type GetResponse struct { + Volume *Volume +} + +// ListResponse structure for a volume list response +type ListResponse struct { + Volumes []*Volume +} + +// CapabilitiesResponse structure for a volume capability response +type CapabilitiesResponse struct { + Capabilities Capability +} + +// Volume represents a volume object for use with `Get` and `List` requests +type Volume struct { + Name string + Mountpoint string `json:",omitempty"` + CreatedAt string `json:",omitempty"` + Status map[string]interface{} `json:",omitempty"` +} + +// Capability represents the list of capabilities a volume driver can return +type Capability struct { + Scope string +} + +// ErrorResponse is a formatted error message that docker can understand +type ErrorResponse struct { + Err string +} + +// NewErrorResponse creates an ErrorResponse with the provided message +func NewErrorResponse(msg string) *ErrorResponse { + return &ErrorResponse{Err: msg} +} + +// Driver represent the interface a driver must fulfill. +type Driver interface { + Create(*CreateRequest) error + List() (*ListResponse, error) + Get(*GetRequest) (*GetResponse, error) + Remove(*RemoveRequest) error + Path(*PathRequest) (*PathResponse, error) + Mount(*MountRequest) (*MountResponse, error) + Unmount(*UnmountRequest) error + Capabilities() *CapabilitiesResponse +} + +// Handler forwards requests and responses between the docker daemon and the plugin. +type Handler struct { + driver Driver + sdk.Handler +} + +// NewHandler initializes the request handler with a driver implementation. +func NewHandler(driver Driver) *Handler { + h := &Handler{driver, sdk.NewHandler(manifest)} + h.initMux() + return h +} + +func (h *Handler) initMux() { + h.HandleFunc(createPath, func(w http.ResponseWriter, r *http.Request) { + log.Println("Entering go-plugins-helpers createPath") + req := &CreateRequest{} + err := sdk.DecodeRequest(w, r, req) + if err != nil { + return + } + err = h.driver.Create(req) + if err != nil { + sdk.EncodeResponse(w, NewErrorResponse(err.Error()), true) + return + } + sdk.EncodeResponse(w, struct{}{}, false) + }) + h.HandleFunc(removePath, func(w http.ResponseWriter, r *http.Request) { + log.Println("Entering go-plugins-helpers removePath") + req := &RemoveRequest{} + err := sdk.DecodeRequest(w, r, req) + if err != nil { + return + } + err = h.driver.Remove(req) + if err != nil { + sdk.EncodeResponse(w, NewErrorResponse(err.Error()), true) + return + } + sdk.EncodeResponse(w, struct{}{}, false) + }) + h.HandleFunc(mountPath, func(w http.ResponseWriter, r *http.Request) { + log.Println("Entering go-plugins-helpers mountPath") + req := &MountRequest{} + err := sdk.DecodeRequest(w, r, req) + if err != nil { + return + } + res, err := h.driver.Mount(req) + if err != nil { + sdk.EncodeResponse(w, NewErrorResponse(err.Error()), true) + return + } + sdk.EncodeResponse(w, res, false) + }) + h.HandleFunc(hostVirtualPath, func(w http.ResponseWriter, r *http.Request) { + log.Println("Entering go-plugins-helpers hostVirtualPath") + req := &PathRequest{} + err := sdk.DecodeRequest(w, r, req) + if err != nil { + return + } + res, err := h.driver.Path(req) + if err != nil { + sdk.EncodeResponse(w, NewErrorResponse(err.Error()), true) + return + } + sdk.EncodeResponse(w, res, false) + }) + h.HandleFunc(getPath, func(w http.ResponseWriter, r *http.Request) { + log.Println("Entering go-plugins-helpers getPath") + req := &GetRequest{} + err := sdk.DecodeRequest(w, r, req) + if err != nil { + return + } + res, err := h.driver.Get(req) + if err != nil { + sdk.EncodeResponse(w, NewErrorResponse(err.Error()), true) + return + } + sdk.EncodeResponse(w, res, false) + }) + h.HandleFunc(unmountPath, func(w http.ResponseWriter, r *http.Request) { + log.Println("Entering go-plugins-helpers unmountPath") + req := &UnmountRequest{} + err := sdk.DecodeRequest(w, r, req) + if err != nil { + return + } + err = h.driver.Unmount(req) + if err != nil { + sdk.EncodeResponse(w, NewErrorResponse(err.Error()), true) + return + } + sdk.EncodeResponse(w, struct{}{}, false) + }) + h.HandleFunc(listPath, func(w http.ResponseWriter, r *http.Request) { + log.Println("Entering go-plugins-helpers listPath") + res, err := h.driver.List() + if err != nil { + sdk.EncodeResponse(w, NewErrorResponse(err.Error()), true) + return + } + sdk.EncodeResponse(w, res, false) + }) + + h.HandleFunc(capabilitiesPath, func(w http.ResponseWriter, r *http.Request) { + log.Println("Entering go-plugins-helpers capabilitiesPath") + sdk.EncodeResponse(w, h.driver.Capabilities(), false) + }) +} 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 e19f94abd..9de30e604 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -211,6 +211,8 @@ github.com/containers/storage/pkg/truncindex github.com/containers/storage/pkg/unshare # github.com/coreos/go-iptables v0.4.5 github.com/coreos/go-iptables/iptables +# github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e +github.com/coreos/go-systemd/activation # github.com/coreos/go-systemd/v22 v22.1.0 github.com/coreos/go-systemd/v22/activation github.com/coreos/go-systemd/v22/daemon @@ -276,6 +278,9 @@ github.com/docker/go-connections/sockets github.com/docker/go-connections/tlsconfig # github.com/docker/go-metrics v0.0.1 github.com/docker/go-metrics +# github.com/docker/go-plugins-helpers v0.0.0-20200102110956-c9a8a2d92ccc +github.com/docker/go-plugins-helpers/sdk +github.com/docker/go-plugins-helpers/volume # github.com/docker/go-units v0.4.0 github.com/docker/go-units # github.com/docker/libnetwork v0.8.0-dev.2.0.20190625141545-5a177b73e316 @@ -512,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 |