Merge pull request #14457 from Luap99/completion4

shell completion for paths inside the image/container
This commit is contained in:
OpenShift Merge Robot 2022-06-02 14:02:11 -04:00 committed by GitHub
commit be527a358a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 186 additions and 22 deletions

View file

@ -4,6 +4,7 @@ import (
"bufio"
"fmt"
"os"
"path"
"reflect"
"strconv"
"strings"
@ -21,6 +22,7 @@ import (
"github.com/containers/podman/v4/pkg/signal"
systemdDefine "github.com/containers/podman/v4/pkg/systemd/define"
"github.com/containers/podman/v4/pkg/util"
securejoin "github.com/cyphar/filepath-securejoin"
"github.com/spf13/cobra"
)
@ -282,6 +284,61 @@ func getNetworks(cmd *cobra.Command, toComplete string, cType completeType) ([]s
return suggestions, cobra.ShellCompDirectiveNoFileComp
}
func getPathCompletion(root string, toComplete string) []string {
if toComplete == "" {
toComplete = "/"
}
// Important: securejoin is required to make sure we never leave the root mount point
userpath, err := securejoin.SecureJoin(root, toComplete)
if err != nil {
cobra.CompErrorln(err.Error())
return nil
}
var base string
f, err := os.Open(userpath)
if err != nil {
// Do not use path.Dir() since this cleans the paths which
// then no longer matches the user input.
userpath, base = path.Split(userpath)
toComplete, _ = path.Split(toComplete)
f, err = os.Open(userpath)
if err != nil {
return nil
}
}
stat, err := f.Stat()
if err != nil {
cobra.CompErrorln(err.Error())
return nil
}
if !stat.IsDir() {
// nothing to complete since it is no dir
return nil
}
entries, err := f.ReadDir(-1)
if err != nil {
cobra.CompErrorln(err.Error())
return nil
}
completions := make([]string, 0, len(entries))
for _, e := range entries {
if strings.HasPrefix(e.Name(), base) {
completions = append(completions, simplePathJoinUnix(toComplete, e.Name()))
}
}
return completions
}
// simplePathJoinUnix joins to path components by adding a slash only if p1 doesn't end with one.
// We cannot use path.Join() for the completions logic because this one always calls Clean() on
// the path which changes it from the input.
func simplePathJoinUnix(p1, p2 string) string {
if p1[len(p1)-1] == '/' {
return p1 + p2
}
return p1 + "/" + p2
}
// validCurrentCmdLine validates the current cmd line
// It utilizes the Args function from the cmd struct
// In most cases the Args function validates the args length but it
@ -523,8 +580,32 @@ func AutocompleteCreateRun(cmd *cobra.Command, args []string, toComplete string)
}
return getImages(cmd, toComplete)
}
// TODO: add path completion for files in the image
return nil, cobra.ShellCompDirectiveDefault
// Mount the image and provide path completion
engine, err := setupImageEngine(cmd)
if err != nil {
cobra.CompErrorln(err.Error())
return nil, cobra.ShellCompDirectiveDefault
}
resp, err := engine.Mount(registry.Context(), []string{args[0]}, entities.ImageMountOptions{})
if err != nil {
cobra.CompErrorln(err.Error())
return nil, cobra.ShellCompDirectiveDefault
}
defer func() {
_, err := engine.Unmount(registry.Context(), []string{args[0]}, entities.ImageUnmountOptions{})
if err != nil {
cobra.CompErrorln(err.Error())
}
}()
if len(resp) != 1 {
return nil, cobra.ShellCompDirectiveDefault
}
// So this uses ShellCompDirectiveDefault to also still provide normal shell
// completion in case no path matches. This is useful if someone tries to get
// completion for paths that are not available in the image, e.g. /proc/...
return getPathCompletion(resp[0].Path, toComplete), cobra.ShellCompDirectiveDefault | cobra.ShellCompDirectiveNoSpace
}
// AutocompleteRegistries - Autocomplete registries.
@ -572,14 +653,39 @@ func AutocompleteCpCommand(cmd *cobra.Command, args []string, toComplete string)
return nil, cobra.ShellCompDirectiveNoFileComp
}
if len(args) < 2 {
if i := strings.IndexByte(toComplete, ':'); i > -1 {
// Looks like the user already set the container.
// Lets mount it and provide path completion for files in the container.
engine, err := setupContainerEngine(cmd)
if err != nil {
cobra.CompErrorln(err.Error())
return nil, cobra.ShellCompDirectiveDefault
}
resp, err := engine.ContainerMount(registry.Context(), []string{toComplete[:i]}, entities.ContainerMountOptions{})
if err != nil {
cobra.CompErrorln(err.Error())
return nil, cobra.ShellCompDirectiveDefault
}
defer func() {
_, err := engine.ContainerUnmount(registry.Context(), []string{toComplete[:i]}, entities.ContainerUnmountOptions{})
if err != nil {
cobra.CompErrorln(err.Error())
}
}()
if len(resp) != 1 {
return nil, cobra.ShellCompDirectiveDefault
}
return prefixSlice(toComplete[:i+1], getPathCompletion(resp[0].Path, toComplete[i+1:])), cobra.ShellCompDirectiveDefault | cobra.ShellCompDirectiveNoSpace
}
// Suggest containers when they match the input otherwise normal shell completion is used
containers, _ := getContainers(cmd, toComplete, completeDefault)
for _, container := range containers {
// TODO: Add path completion for inside the container if possible
if strings.HasPrefix(container, toComplete) {
return containers, cobra.ShellCompDirectiveNoSpace
return suffixCompSlice(":", containers), cobra.ShellCompDirectiveNoSpace
}
}
// else complete paths
// else complete paths on the host
return nil, cobra.ShellCompDirectiveDefault
}
// don't complete more than 2 args

View file

@ -8,6 +8,16 @@
load helpers
function setup() {
# $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
# set the var for the run_completion function
PODMAN_COMPLETION="${podman_as_array[0]} __completeNoDesc ${podman_as_array[@]:1}"
basic_setup
}
# Returns true if we are able to podman-pause
function _can_pause() {
# Even though we're just trying completion, not an actual unpause,
@ -88,8 +98,14 @@ function check_shell_completion() {
continue 2
fi
name=$random_container_name
# special case podman cp suggest containers names with a colon
if [[ $cmd = "cp" ]]; then
name="$name:"
fi
run_completion "$@" $cmd "${extra_args[@]}" ""
is "$output" ".*-$random_container_name${nl}" \
is "$output" ".*-$name${nl}" \
"$* $cmd: actual container listed in suggestions"
match=true
@ -175,7 +191,7 @@ function check_shell_completion() {
_check_completion_end NoSpace
else
_check_completion_end Default
assert "${#lines[@]}" -eq 2 "$* $cmd: Suggestions are in the output"
_check_no_suggestions
fi
;;
@ -205,16 +221,7 @@ function check_shell_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 include additional debug output
# lines starting with [Debug] are allowed
i=0
length=$(( ${#lines[@]} - 2 ))
while [[ i -lt length ]]; do
assert "${lines[$i]:0:7}" == "[Debug]" "Suggestions are in the output"
i=$(( i + 1 ))
done
fi
_check_no_suggestions
fi
done
@ -231,6 +238,24 @@ function _check_completion_end() {
is "${lines[-1]}" "Completion ended with directive: ShellCompDirective$1" "Completion has wrong ShellCompDirective set"
}
# Check that there are no suggestions in the output.
# We could only check stdout and not stderr but this is not possible with bats.
# By default we always have two extra lines at the end for the ShellCompDirective.
# Then we could also have other extra lines for debugging, they will always start
# with [Debug], e.g. `[Debug] [Error] no container with name or ID "t12" found: no such container`.
function _check_no_suggestions() {
if [ ${#lines[@]} -gt 2 ]; then
# Checking for line count is not enough since we may include additional debug output.
# Lines starting with [Debug] are allowed.
local i=0
length=$((${#lines[@]} - 2))
while [[ i -lt length ]]; do
assert "${lines[$i]:0:7}" == "[Debug]" "Unexpected non-Debug output line: ${lines[$i]}"
i=$((i + 1))
done
fi
}
@test "podman shell completion test" {
@ -280,11 +305,6 @@ function _check_completion_end() {
# create secret
run_podman secret create $random_secret_name $secret_file
# $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
@ -316,3 +336,41 @@ function _check_completion_end() {
done <<<"$output"
}
@test "podman shell completion for paths in container/image" {
skip_if_remote "mounting via remote does not work"
for cmd in create run; do
run_completion $cmd $IMAGE ""
assert "$output" =~ ".*^/etc\$.*^/home\$.*^/root\$.*" "root directories suggested (cmd: podman $cmd)"
# check completion for subdirectory
run_completion $cmd $IMAGE "/etc"
# It should be safe to assume the os-release file always exists in $IMAGE
assert "$output" =~ ".*^/etc/os-release\$.*" "/etc files suggested (cmd: podman $cmd /etc)"
# check completion for partial file name
run_completion $cmd $IMAGE "/etc/os-"
assert "$output" =~ ".*^/etc/os-release\$.*" "/etc files suggested (cmd: podman $cmd /etc/os-)"
# check completion with relative path components
# It is important the we will still use the image root and not escape to the host
run_completion $cmd $IMAGE "../../"
assert "$output" =~ ".*^../../etc\$.*^../../home\$.*" "relative root directories suggested (cmd: podman $cmd ../../)"
done
random_name=$(random_string 30)
random_file=$(random_string 30)
run_podman run --name $random_name $IMAGE touch /tmp/$random_file
# check completion for podman cp
run_completion cp ""
assert "$output" =~ ".*^$random_name\:\$.*" "podman cp suggest container names"
run_completion cp "$random_name:"
assert "$output" =~ ".*^$random_name\:/etc\$.*" "podman cp suggest paths in container"
run_completion cp "$random_name:/tmp"
assert "$output" =~ ".*^$random_name\:/tmp/$random_file\$.*" "podman cp suggest custom file in container"
# cleanup container
run_podman rm $random_name
}