Merge pull request #8635 from Luap99/shell-completion-test

Add system test for shell completion
This commit is contained in:
OpenShift Merge Robot
2020-12-09 14:47:05 -05:00
committed by GitHub
25 changed files with 392 additions and 36 deletions

View File

@ -278,7 +278,6 @@ func validCurrentCmdLine(cmd *cobra.Command, args []string, toComplete string) b
return true
}
}
cobra.CompDebugln(err.Error(), true)
return false
}
return true
@ -445,6 +444,29 @@ func AutocompleteNetworks(cmd *cobra.Command, args []string, toComplete string)
return getNetworks(cmd, toComplete)
}
// AutocompleteDefaultOneArg - Autocomplete path only for the first argument.
func AutocompleteDefaultOneArg(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
if len(args) == 0 {
return nil, cobra.ShellCompDirectiveDefault
}
return nil, cobra.ShellCompDirectiveNoFileComp
}
// AutocompleteCommitCommand - Autocomplete podman commit command args.
func AutocompleteCommitCommand(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
if !validCurrentCmdLine(cmd, args, toComplete) {
return nil, cobra.ShellCompDirectiveNoFileComp
}
if len(args) == 0 {
return getContainers(cmd, toComplete, completeDefault)
}
if len(args) == 1 {
return getImages(cmd, toComplete)
}
// don't complete more than 2 args
return nil, cobra.ShellCompDirectiveNoFileComp
}
// AutocompleteCpCommand - Autocomplete podman cp command args.
func AutocompleteCpCommand(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
if !validCurrentCmdLine(cmd, args, toComplete) {
@ -465,6 +487,43 @@ func AutocompleteCpCommand(cmd *cobra.Command, args []string, toComplete string)
return nil, cobra.ShellCompDirectiveNoFileComp
}
// AutocompleteExecCommand - Autocomplete podman exec command args.
func AutocompleteExecCommand(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
if !validCurrentCmdLine(cmd, args, toComplete) {
return nil, cobra.ShellCompDirectiveNoFileComp
}
if len(args) == 0 {
return getContainers(cmd, toComplete, completeDefault, "running")
}
return nil, cobra.ShellCompDirectiveDefault
}
// AutocompleteRunlabelCommand - Autocomplete podman container runlabel command args.
func AutocompleteRunlabelCommand(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
if !validCurrentCmdLine(cmd, args, toComplete) {
return nil, cobra.ShellCompDirectiveNoFileComp
}
if len(args) == 0 {
// FIXME: What labels can we recommend here?
return nil, cobra.ShellCompDirectiveNoFileComp
}
if len(args) == 1 {
return getImages(cmd, toComplete)
}
return nil, cobra.ShellCompDirectiveDefault
}
// AutocompletePortCommand - Autocomplete podman port command args.
func AutocompletePortCommand(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
if !validCurrentCmdLine(cmd, args, toComplete) {
return nil, cobra.ShellCompDirectiveNoFileComp
}
if len(args) == 0 {
return getContainers(cmd, toComplete, completeDefault)
}
return nil, cobra.ShellCompDirectiveNoFileComp
}
// AutocompleteNetworkConnectCmd - Autocomplete podman network connect/disconnect command args.
func AutocompleteNetworkConnectCmd(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
if len(args) == 0 {
@ -496,6 +555,23 @@ func AutocompleteTopCmd(cmd *cobra.Command, args []string, toComplete string) ([
return descriptors, cobra.ShellCompDirectiveNoFileComp
}
// AutocompleteInspect - Autocomplete podman inspect.
func AutocompleteInspect(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
if !validCurrentCmdLine(cmd, args, toComplete) {
return nil, cobra.ShellCompDirectiveNoFileComp
}
containers, _ := getContainers(cmd, toComplete, completeDefault)
images, _ := getImages(cmd, toComplete)
pods, _ := getPods(cmd, toComplete, completeDefault)
networks, _ := getNetworks(cmd, toComplete)
volumes, _ := getVolumes(cmd, toComplete)
suggestions := append(containers, images...)
suggestions = append(suggestions, pods...)
suggestions = append(suggestions, networks...)
suggestions = append(suggestions, volumes...)
return suggestions, cobra.ShellCompDirectiveNoFileComp
}
// AutocompleteSystemConnections - Autocomplete system connections.
func AutocompleteSystemConnections(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
if !validCurrentCmdLine(cmd, args, toComplete) {

View File

@ -24,7 +24,7 @@ var (
Long: commitDescription,
RunE: commit,
Args: cobra.RangeArgs(1, 2),
ValidArgsFunction: common.AutocompleteContainers,
ValidArgsFunction: common.AutocompleteCommitCommand,
Example: `podman commit -q --message "committing container to image" reverent_golick image-committed
podman commit -q --author "firstName lastName" reverent_golick image-committed
podman commit -q --pause=false containerID image-committed

View File

@ -13,7 +13,7 @@ var (
You can copy from the container's file system to the local machine or the reverse, from the local filesystem to the container. If "-" is specified for either the SRC_PATH or DEST_PATH, you can also stream a tar archive from STDIN or to STDOUT. The CONTAINER can be a running or stopped container. The SRC_PATH or DEST_PATH can be a file or directory.
`
cpCommand = &cobra.Command{
Use: "cp [options] SRC_PATH DEST_PATH",
Use: "cp [options] [CONTAINER:]SRC_PATH [CONTAINER:]DEST_PATH",
Short: "Copy files/folders between a container and the local filesystem",
Long: cpDescription,
Args: cobra.ExactArgs(2),

View File

@ -26,7 +26,7 @@ var (
Long: execDescription,
RunE: exec,
DisableFlagsInUseLine: true,
ValidArgsFunction: common.AutocompleteContainersRunning,
ValidArgsFunction: common.AutocompleteExecCommand,
Example: `podman exec -it ctrID ls
podman exec -it -w /tmp myCtr pwd
podman exec --user root ctrID ls`,

View File

@ -69,6 +69,12 @@ var (
)
func init() {
// if run remotely we only allow one container arg
if registry.IsRemote() {
logsCommand.Use = "logs [options] CONTAINER"
containerLogsCommand.Use = logsCommand.Use
}
// logs
registry.Commands = append(registry.Commands, registry.CliCommand{
Mode: []entities.EngineMode{entities.ABIMode, entities.TunnelMode},

View File

@ -26,7 +26,7 @@ var (
Args: func(cmd *cobra.Command, args []string) error {
return validate.CheckAllLatestAndCIDFile(cmd, args, true, false)
},
ValidArgsFunction: common.AutocompleteContainers,
ValidArgsFunction: common.AutocompletePortCommand,
Example: `podman port --all
podman port ctrID 80/tcp
podman port --latest 80`,

View File

@ -30,7 +30,7 @@ var (
Long: runlabelDescription,
RunE: runlabel,
Args: cobra.MinimumNArgs(2),
ValidArgsFunction: common.AutocompleteImages,
ValidArgsFunction: common.AutocompleteRunlabelCommand,
Example: `podman container runlabel run imageID
podman container runlabel install imageID arg1 arg2
podman container runlabel --display run myImage`,

View File

@ -18,7 +18,7 @@ var (
// Command: podman _diff_ Object_ID
diffDescription = `Displays changes on a container or image's filesystem. The container or image will be compared to its parent layer.`
diffCmd = &cobra.Command{
Use: "diff [options] {CONTAINER_ID | IMAGE_ID}",
Use: "diff [options] {CONTAINER|IMAGE}",
Args: validate.IDOrLatestArgs,
Short: "Display the changes to the object's file system",
Long: diffDescription,

View File

@ -22,7 +22,7 @@ var (
Whether the input is for a container or pod, Podman will always generate the specification as a pod.`
kubeCmd = &cobra.Command{
Use: "kube [options] CONTAINER... | POD",
Use: "kube [options] {CONTAINER...|POD}",
Short: "Generate Kubernetes YAML from a container or pod.",
Long: kubeDescription,
RunE: kube,

View File

@ -26,7 +26,7 @@ var (
The generated units can later be controlled via systemctl(1).`
systemdCmd = &cobra.Command{
Use: "systemd [options] CTR|POD",
Use: "systemd [options] {CONTAINER|POD}",
Short: "Generate systemd units.",
Long: systemdDescription,
RunE: systemd,

View File

@ -11,6 +11,7 @@ import (
"github.com/containers/buildah/pkg/parse"
"github.com/containers/common/pkg/completion"
"github.com/containers/common/pkg/config"
"github.com/containers/podman/v2/cmd/podman/common"
"github.com/containers/podman/v2/cmd/podman/registry"
"github.com/containers/podman/v2/cmd/podman/utils"
"github.com/containers/podman/v2/pkg/domain/entities"
@ -44,7 +45,7 @@ var (
Long: buildDescription,
Args: cobra.MaximumNArgs(1),
RunE: build,
ValidArgsFunction: completion.AutocompleteDefault,
ValidArgsFunction: common.AutocompleteDefaultOneArg,
Example: `podman build .
podman build --creds=username:password -t imageName -f Containerfile.simple .
podman build --layers --force-rm --tag imageName .`,

View File

@ -25,18 +25,19 @@ var (
Short: "Import a tarball to create a filesystem image",
Long: importDescription,
RunE: importCon,
ValidArgsFunction: completion.AutocompleteDefault,
Args: cobra.RangeArgs(1, 2),
ValidArgsFunction: common.AutocompleteDefaultOneArg,
Example: `podman import http://example.com/ctr.tar url-image
cat ctr.tar | podman -q import --message "importing the ctr.tar tarball" - image-imported
cat ctr.tar | podman import -`,
}
imageImportCommand = &cobra.Command{
Args: cobra.MinimumNArgs(1),
Use: importCommand.Use,
Short: importCommand.Short,
Long: importCommand.Long,
RunE: importCommand.RunE,
Args: importCommand.Args,
ValidArgsFunction: importCommand.ValidArgsFunction,
Example: `podman image import http://example.com/ctr.tar url-image
cat ctr.tar | podman -q image import --message "importing the ctr.tar tarball" - image-imported

View File

@ -29,7 +29,7 @@ var (
// Command: podman push
pushCmd = &cobra.Command{
Use: "push [options] SOURCE [DESTINATION]",
Use: "push [options] IMAGE [DESTINATION]",
Short: "Push an image to a specified destination",
Long: pushDescription,
RunE: imagePush,

View File

@ -43,7 +43,7 @@ var (
}
return nil
},
ValidArgsFunction: completion.AutocompleteNone,
ValidArgsFunction: common.AutocompleteImages,
Example: `podman save --quiet -o myimage.tar imageID
podman save --format docker-dir -o ubuntu-dir ubuntu
podman save > alpine-all.tar alpine:latest`,

View File

@ -9,7 +9,7 @@ import (
var (
untagCommand = &cobra.Command{
Use: "untag IMAGE [NAME...]",
Use: "untag IMAGE [IMAGE...]",
Short: "Remove a name from a local image",
Long: "Removes one or more names from a locally-stored image.",
RunE: untag,

View File

@ -20,12 +20,12 @@ var (
// Command: podman _inspect_ Object_ID
inspectCmd = &cobra.Command{
Use: "inspect [options] {CONTAINER_ID | IMAGE_ID} [...]",
Use: "inspect [options] {CONTAINER|IMAGE|POD|NETWORK|VOLUME} [...]",
Short: "Display the configuration of object denoted by ID",
RunE: inspectExec,
Long: inspectDescription,
TraverseChildren: true,
ValidArgsFunction: common.AutocompleteContainersAndImages,
ValidArgsFunction: common.AutocompleteInspect,
Example: `podman inspect fedora
podman inspect --type image fedora
podman inspect CtrID ImgID

View File

@ -24,7 +24,7 @@ type manifestPushOptsWrapper struct {
var (
manifestPushOpts = manifestPushOptsWrapper{}
pushCmd = &cobra.Command{
Use: "push [options] SOURCE DESTINATION",
Use: "push [options] LIST DESTINATION",
Short: "Push a manifest list or image index to a registry",
Long: "Pushes manifest lists and image indexes to registries.",
RunE: push,

View File

@ -17,7 +17,7 @@ import (
var (
networkCreateDescription = `create CNI networks for containers and pods`
networkCreateCommand = &cobra.Command{
Use: "create [options] [NETWORK]",
Use: "create [options] [NAME]",
Short: "network create",
Long: networkCreateDescription,
RunE: networkCreate,

View File

@ -39,7 +39,7 @@ var (
Long: kubeDescription,
RunE: kube,
Args: cobra.ExactArgs(1),
ValidArgsFunction: completion.AutocompleteDefault,
ValidArgsFunction: common.AutocompleteDefaultOneArg,
Example: `podman play kube nginx.yml
podman play kube --creds user:password --seccomp-profile-root /custom/path apache.yml`,
}

View File

@ -7,7 +7,7 @@ import (
"os"
"strings"
"github.com/containers/podman/v2/cmd/podman/common"
"github.com/containers/common/pkg/completion"
"github.com/containers/podman/v2/cmd/podman/registry"
"github.com/containers/podman/v2/cmd/podman/utils"
"github.com/containers/podman/v2/cmd/podman/validate"
@ -28,7 +28,7 @@ var (
Short: "Remove all stopped pods and their containers",
Long: pruneDescription,
RunE: prune,
ValidArgsFunction: common.AutocompletePods,
ValidArgsFunction: completion.AutocompleteNone,
Example: `podman pod prune`,
}
)

View File

@ -10,6 +10,7 @@ import (
"time"
"github.com/containers/common/pkg/completion"
"github.com/containers/podman/v2/cmd/podman/common"
"github.com/containers/podman/v2/cmd/podman/registry"
"github.com/containers/podman/v2/pkg/domain/entities"
"github.com/containers/podman/v2/pkg/rootless"
@ -32,7 +33,7 @@ Enable a listening service for API access to Podman commands.
Short: "Run API service",
Long: srvDescription,
RunE: service,
ValidArgsFunction: completion.AutocompleteDefault,
ValidArgsFunction: common.AutocompleteDefaultOneArg,
Example: `podman system service --time=0 unix:///tmp/podman.sock`,
}

View File

@ -14,7 +14,7 @@ import (
var (
unshareDescription = "Runs a command in a modified user namespace."
unshareCommand = &cobra.Command{
Use: "unshare [COMMAND [ARG ...]]",
Use: "unshare [COMMAND [ARG...]]",
DisableFlagsInUseLine: true,
Short: "Run a command in a modified user namespace",
Long: unshareDescription,

View File

@ -12,22 +12,11 @@
#
load helpers
# run 'podman help', parse the output looking for 'Available Commands';
# return that list.
function podman_commands() {
dprint "$@"
run_podman help "$@" |\
awk '/^Available Commands:/{ok=1;next}/^Options:/{ok=0}ok { print $1 }' |\
grep .
"$output"
}
function check_help() {
local count=0
local -A found
for cmd in $(podman_commands "$@"); do
for cmd in $(_podman_commands "$@"); do
# Human-readable podman command string, with multiple spaces collapsed
command_string="podman $* $cmd"
command_string=${command_string// / } # 'podman x' -> 'podman x'

View File

@ -0,0 +1,272 @@
#!/usr/bin/env bats -*- bats -*-
#
# Test podman shell completion
#
# Shell completion is provided via the cobra library
# It is implement by calling a hidden subcommand called "__complete"
#
load helpers
function check_shell_completion() {
local count=0
# Newline character; used for confirming string output
local nl="
"
for cmd in $(_podman_commands "$@"); do
# Human-readable podman command string, with multiple spaces collapsed
name="podman"
if is_remote; then
name="podman-remote"
fi
command_string="$name $* $cmd"
command_string=${command_string// / } # 'podman x' -> 'podman x'
run_podman "$@" $cmd --help
local full_help="$output"
# The line immediately after 'Usage:' gives us a 1-line synopsis
usage=$(echo "$full_help" | grep -A1 '^Usage:' | tail -1)
[ -n "$usage" ] || die "podman $cmd: no Usage message found"
# If usage ends in '[command]', recurse into subcommands
if expr "$usage" : '.*\[command\]$' >/dev/null; then
check_shell_completion "$@" $cmd
continue
fi
# Trim to command path so we only have the args
args="${usage/$command_string/}"
# Trim leading whitespaces
args="${args#"${args%%[![:space:]]*}"}"
# Extra args is used to match the correct argument number for the command
# This is important because some commands provide different suggestions based
# on the number of arguments.
extra_args=()
for arg in $args; do
match=false
i=0
while true; do
case $arg in
# If we have options than we need to check if we are getting flag completion
"[options]")
# skip this for remote it fails if a command only has the latest flag e.g podman top
if ! is_remote; then
run_completion "$@" $cmd "--"
# If this fails there is most likely a problem with the cobra library
is "${lines[0]}" "--.*" "Found flag in suggestions"
[ ${#lines[@]} -gt 2 ] || die "No flag suggestions"
_check_completion_end NoFileComp
fi
# continue the outer for args loop
continue 2
;;
*CONTAINER*)
run_completion "$@" $cmd "${extra_args[@]}" ""
is "$output" ".*-$random_container_name${nl}" "Found expected container in suggestions"
match=true
# resume
;;&
*POD*)
run_completion "$@" $cmd "${extra_args[@]}" ""
is "$output" ".*-$random_pod_name${nl}" "Found expected pod in suggestions"
_check_completion_end NoFileComp
match=true
# resume
;;&
*IMAGE*)
run_completion "$@" $cmd "${extra_args[@]}" ""
is "$output" ".*localhost/$random_image_name:$random_image_tag${nl}" "Found expected image in suggestions"
# check that we complete the image with and without tag after at least one char is typed
run_completion "$@" $cmd "${extra_args[@]}" "${random_image_name:0:1}"
is "$output" ".*$random_image_name:$random_image_tag${nl}" "Found expected image with tag in suggestions"
is "$output" ".*$random_image_name${nl}" "Found expected image without tag in suggestions"
# check that we complete the image id after at least two chars are typed
run_completion "$@" $cmd "${extra_args[@]}" "${random_image_id:0:2}"
is "$output" ".*$random_image_id${nl}" "Found expected image id in suggestions"
match=true
# resume
;;&
*NETWORK*)
run_completion "$@" $cmd "${extra_args[@]}" ""
is "$output" ".*$random_network_name${nl}" "Found network in suggestions"
_check_completion_end NoFileComp
match=true
# resume
;;&
*VOLUME*)
run_completion "$@" $cmd "${extra_args[@]}" ""
is "$output" ".*$random_volume_name${nl}" "Found volume in suggestions"
_check_completion_end NoFileComp
match=true
# resume
;;&
*REGISTRY*)
run_completion "$@" $cmd "${extra_args[@]}" ""
### FIXME how can we get the configured registries?
_check_completion_end NoFileComp
### FIXME this fails if no registries are configured
[[ ${#lines[@]} -gt 2 ]] || die "No registries found in suggestions"
match=true
# resume
;;&
*PATH* | *CONTEXT* | *KUBEFILE* | *COMMAND* | *ARG...* | *URI*)
# default shell completion should be done for everthing which accepts a path
run_completion "$@" $cmd "${extra_args[@]}" ""
# cp is a special case it returns ShellCompDirectiveNoSpace
if [[ "$cmd" == "cp" ]]; then
_check_completion_end NoSpace
else
_check_completion_end Default
[[ ${#lines[@]} -eq 2 ]] || die "Suggestions are in the output"
fi
;;
*)
if [[ "$match" == "false" ]]; then
dprint "UNKNOWN arg: $arg for $command_string ${extra_args[*]}"
fi
;;
esac
# Increment the argument array
extra_args+=("arg")
i=$(($i + 1))
# If the argument ends with ...] than we accept 0...n args
# Loop three times to make sure we are not only completing the first arg
if [[ ! ${arg} =~ "..." ]] || [[ i -gt 3 ]]; then
break
fi
done
done
# If the command takes no more parameters make sure we are getting no completion
if [[ ! ${args##* } =~ "..." ]]; then
run_completion "$@" $cmd "${extra_args[@]}" ""
_check_completion_end NoFileComp
if [ ${#lines[@]} -gt 2 ]; then
# checking for line count is not enough since we may inlcude additional debug output
# lines starting with [Debug] are allowed
i=0
length=$(( ${#lines[@]} - 2 ))
while [[ i -lt length ]]; do
[[ "${lines[$i]:0:7}" == "[Debug]" ]] || die "Suggestions are in the output"
i=$(( i + 1 ))
done
fi
fi
done
}
# run the completion cmd
function run_completion() {
PODMAN="$PODMAN_COMPLETION" run_podman "$@"
}
# check for the given ShellCompDirective (always last line)
function _check_completion_end() {
is "${lines[-1]}" "Completion ended with directive: ShellCompDirective$1" "Completion has wrong ShellCompDirective set"
}
@test "podman shell completion test" {
random_container_name=$(random_string 30)
random_pod_name=$(random_string 30)
random_image_name=$(random_string 30)
random_image_name=${random_image_name,,} # name must be lowercase
random_image_tag=$(random_string 5)
random_network_name=$(random_string 30)
random_volume_name=$(random_string 30)
# create a container for each state since some commands are only suggesting running container for example
run_podman create --name created-$random_container_name $IMAGE
run_podman run --name running-$random_container_name -d $IMAGE top
run_podman run --name pause-$random_container_name -d $IMAGE top
run_podman pause pause-$random_container_name
run_podman run --name exited-$random_container_name -d $IMAGE echo exited
# create pods for each state
run_podman pod create --name created-$random_pod_name
run_podman pod create --name running-$random_pod_name
run_podman run -d --name running-$random_pod_name-con --pod running-$random_pod_name $IMAGE top
run_podman pod create --name degraded-$random_pod_name
run_podman run -d --name degraded-$random_pod_name-con --pod degraded-$random_pod_name $IMAGE echo degraded
run_podman pod create --name exited-$random_pod_name
run_podman run -d --name exited-$random_pod_name-con --pod exited-$random_pod_name $IMAGE echo exited
run_podman pod stop exited-$random_pod_name
# create image name (just tag with new names no need to pull)
run_podman image tag $IMAGE $random_image_name:$random_image_tag
run_podman image list --format '{{.ID}}' --filter reference=$random_image_name
random_image_id="${lines[0]}"
# create network
run_podman network create $random_network_name
# create volume
run_podman volume create $random_volume_name
# $PODMAN may be a space-separated string, e.g. if we include a --url.
local -a podman_as_array=($PODMAN)
# __completeNoDesc must be the first arg if we running the completion cmd
PODMAN_COMPLETION="${podman_as_array[0]} __completeNoDesc ${podman_as_array[@]:1}"
# Called with no args -- start with 'podman --help'. check_shell_completion() will
# recurse for any subcommands.
check_shell_completion
# cleanup
run_podman volume rm $random_volume_name
run_podman network rm $random_network_name
run_podman image untag $IMAGE $random_image_name:$random_image_tag
for state in created running degraded exited; do
run_podman pod rm --force $state-$random_pod_name
done
for state in created running pause exited; do
run_podman rm --force $state-$random_container_name
done
# Clean up the pod pause image
run_podman image list --format '{{.ID}} {{.Repository}}'
while read id name; do
if [[ "$name" =~ /pause ]]; then
run_podman rmi $id
fi
done <<<"$output"
}

View File

@ -521,5 +521,15 @@ function remove_same_dev_warning() {
output=$(printf '%s\n' "${lines[@]}")
}
# run 'podman help', parse the output looking for 'Available Commands';
# return that list.
function _podman_commands() {
dprint "$@"
run_podman help "$@" |
awk '/^Available Commands:/{ok=1;next}/^Options:/{ok=0}ok { print $1 }' |
grep .
"$output"
}
# END miscellaneous tools
###############################################################################