diff options
Diffstat (limited to 'cmd/podman/common/completion.go')
-rw-r--r-- | cmd/podman/common/completion.go | 347 |
1 files changed, 301 insertions, 46 deletions
diff --git a/cmd/podman/common/completion.go b/cmd/podman/common/completion.go index 58dff3578..6e6c33f9b 100644 --- a/cmd/podman/common/completion.go +++ b/cmd/podman/common/completion.go @@ -4,9 +4,13 @@ import ( "bufio" "fmt" "os" + "path" "reflect" + "strconv" "strings" + "unicode" + libimageDefine "github.com/containers/common/libimage/define" "github.com/containers/common/libnetwork/types" "github.com/containers/common/pkg/config" "github.com/containers/image/v5/pkg/sysregistriesv2" @@ -15,8 +19,10 @@ import ( "github.com/containers/podman/v4/libpod/events" "github.com/containers/podman/v4/pkg/domain/entities" "github.com/containers/podman/v4/pkg/rootless" + "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" ) @@ -65,7 +71,7 @@ func setupImageEngine(cmd *cobra.Command) (entities.ImageEngine, error) { return nil, err } // we also need to set up the container engine since this - // is required to setup the rootless namespace + // is required to set up the rootless namespace if _, err = setupContainerEngine(cmd); err != nil { return nil, err } @@ -278,6 +284,90 @@ func getNetworks(cmd *cobra.Command, toComplete string, cType completeType) ([]s return suggestions, cobra.ShellCompDirectiveNoFileComp } +func fdIsNotDir(f *os.File) bool { + stat, err := f.Stat() + if err != nil { + cobra.CompErrorln(err.Error()) + return true + } + return !stat.IsDir() +} + +func getPathCompletion(root string, toComplete string) ([]string, cobra.ShellCompDirective) { + 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, cobra.ShellCompDirectiveDefault + } + var base string + f, err := os.Open(userpath) + // when error or file is not dir get the parent path to stat + if err != nil || fdIsNotDir(f) { + // 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, cobra.ShellCompDirectiveDefault + } + } + + if fdIsNotDir(f) { + // nothing to complete since it is no dir + return nil, cobra.ShellCompDirectiveDefault + } + + entries, err := f.ReadDir(-1) + if err != nil { + cobra.CompErrorln(err.Error()) + return nil, cobra.ShellCompDirectiveDefault + } + if len(entries) == 0 { + // path is empty dir, just add the trailing slash and no space + if !strings.HasSuffix(toComplete, "/") { + toComplete += "/" + } + return []string{toComplete}, cobra.ShellCompDirectiveDefault | cobra.ShellCompDirectiveNoSpace + } + completions := make([]string, 0, len(entries)) + count := 0 + for _, e := range entries { + if strings.HasPrefix(e.Name(), base) { + suf := "" + // When the entry is an directory we add the "/" as suffix and do not want to add space + // to match normal shell completion behavior. + // Just inc counter again to fake more than one entry in this case and thus get no space. + if e.IsDir() { + suf = "/" + count++ + } + completions = append(completions, simplePathJoinUnix(toComplete, e.Name()+suf)) + count++ + } + } + directive := cobra.ShellCompDirectiveDefault + if count > 1 { + // when we have more than one match we do not want to add a space after the completion + directive |= cobra.ShellCompDirectiveNoSpace + } + return completions, directive +} + +// 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 @@ -430,6 +520,16 @@ func AutocompletePodsRunning(cmd *cobra.Command, args []string, toComplete strin return getPods(cmd, toComplete, completeDefault, "running", "degraded") } +// AutoCompletePodsPause - Autocomplete only paused pod names +// When a pod has a few containers paused, that ends up in degraded state +// So autocomplete degraded pod names as well +func AutoCompletePodsPause(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + if !validCurrentCmdLine(cmd, args, toComplete) { + return nil, cobra.ShellCompDirectiveNoFileComp + } + return getPods(cmd, toComplete, completeDefault, "paused", "degraded") +} + // AutocompleteForKube - Autocomplete all Podman objects supported by kube generate. func AutocompleteForKube(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { if !validCurrentCmdLine(cmd, args, toComplete) { @@ -495,6 +595,11 @@ func AutocompleteImages(cmd *cobra.Command, args []string, toComplete string) ([ return getImages(cmd, toComplete) } +// AutocompleteImageSearchFilters - Autocomplate `search --filter`. +func AutocompleteImageSearchFilters(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + return libimageDefine.SearchFilters, cobra.ShellCompDirectiveNoFileComp +} + // AutocompletePodExitPolicy - Autocomplete pod exit policy. func AutocompletePodExitPolicy(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { return config.PodExitPolicies, cobra.ShellCompDirectiveNoFileComp @@ -514,8 +619,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) } // AutocompleteRegistries - Autocomplete registries. @@ -563,14 +692,40 @@ 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 + } + comps, directive := getPathCompletion(resp[0].Path, toComplete[i+1:]) + return prefixSlice(toComplete[:i+1], comps), directive + } + // 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 @@ -594,7 +749,9 @@ func AutocompleteRunlabelCommand(cmd *cobra.Command, args []string, toComplete s return nil, cobra.ShellCompDirectiveNoFileComp } if len(args) == 0 { - // FIXME: What labels can we recommend here? + // This is unfortunate because the argument order is label followed by image. + // If it would be the other way around we could inspect the first arg and get + // all labels from it to suggest them. return nil, cobra.ShellCompDirectiveNoFileComp } if len(args) == 1 { @@ -799,8 +956,7 @@ func AutocompleteLogDriver(cmd *cobra.Command, args []string, toComplete string) // AutocompleteLogOpt - Autocomplete log-opt options. // -> "path=", "tag=" func AutocompleteLogOpt(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - // FIXME: are these the only one? the man page states these but in the current shell completion they are more options - logOptions := []string{"path=", "tag="} + logOptions := []string{"path=", "tag=", "max-size="} if strings.HasPrefix(toComplete, "path=") { return nil, cobra.ShellCompDirectiveDefault } @@ -810,7 +966,7 @@ func AutocompleteLogOpt(cmd *cobra.Command, args []string, toComplete string) ([ // AutocompletePullOption - Autocomplete pull options for create and run command. // -> "always", "missing", "never" func AutocompletePullOption(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - pullOptions := []string{"always", "missing", "never"} + pullOptions := []string{"always", "missing", "never", "newer"} return pullOptions, cobra.ShellCompDirectiveNoFileComp } @@ -839,10 +995,26 @@ func AutocompleteSecurityOption(cmd *cobra.Command, args []string, toComplete st } // AutocompleteStopSignal - Autocomplete stop signal options. -// -> "SIGHUP", "SIGINT", "SIGKILL", "SIGTERM" +// Autocompletes signals both lower or uppercase depending on the user input. func AutocompleteStopSignal(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - // FIXME: add more/different signals? - stopSignals := []string{"SIGHUP", "SIGINT", "SIGKILL", "SIGTERM"} + // convertCase will convert a string to lowercase only if the user input is lowercase + convertCase := func(s string) string { return s } + if len(toComplete) > 0 && unicode.IsLower(rune(toComplete[0])) { + convertCase = strings.ToLower + } + + prefix := "" + // if input starts with "SI" we have to add SIG in front + // since the signal map does not have this prefix but the option + // allows signals with and without SIG prefix + if strings.HasPrefix(toComplete, convertCase("SI")) { + prefix = "SIG" + } + + stopSignals := make([]string, 0, len(signal.SignalMap)) + for sig := range signal.SignalMap { + stopSignals = append(stopSignals, convertCase(prefix+sig)) + } return stopSignals, cobra.ShellCompDirectiveNoFileComp } @@ -963,9 +1135,22 @@ func AutocompleteNetworkFlag(cmd *cobra.Command, args []string, toComplete strin return append(networks, suggestions...), dir } +type formatSuggestion struct { + fieldname string + suffix string +} + +func convertFormatSuggestions(suggestions []formatSuggestion) []string { + completions := make([]string, 0, len(suggestions)) + for _, f := range suggestions { + completions = append(completions, f.fieldname+f.suffix) + } + return completions +} + // AutocompleteFormat - Autocomplete json or a given struct to use for a go template. // The input can be nil, In this case only json will be autocompleted. -// This function will only work for structs other types are not supported. +// This function will only work for pointer to structs other types are not supported. // When "{{." is typed the field and method names of the given struct will be completed. // This also works recursive for nested structs. func AutocompleteFormat(o interface{}) func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { @@ -994,6 +1179,12 @@ func AutocompleteFormat(o interface{}) func(cmd *cobra.Command, args []string, t // split this into it struct field names fields := strings.Split(field[len(field)-1], ".") f := reflect.ValueOf(o) + if f.Kind() != reflect.Ptr { + // We panic here to make sure that all callers pass the value by reference. + // If someone passes a by value then all podman commands will panic since + // this function is run at init time. + panic("AutocompleteFormat: passed value must be a pointer to a struct") + } for i := 1; i < len(fields); i++ { // last field get all names to suggest if i == len(fields)-1 { @@ -1002,61 +1193,83 @@ func AutocompleteFormat(o interface{}) func(cmd *cobra.Command, args []string, t toCompArr := strings.Split(toComplete, ".") toCompArr[len(toCompArr)-1] = "" toComplete = strings.Join(toCompArr, ".") - return prefixSlice(toComplete, suggestions), cobra.ShellCompDirectiveNoSpace | cobra.ShellCompDirectiveNoFileComp + return prefixSlice(toComplete, convertFormatSuggestions(suggestions)), cobra.ShellCompDirectiveNoSpace | cobra.ShellCompDirectiveNoFileComp } - val := getActualStructType(f) - if val == nil { - // no struct return nothing to complete + // first follow pointer and create element when it is nil + f = actualReflectValue(f) + switch f.Kind() { + case reflect.Struct: + for j := 0; j < f.NumField(); j++ { + field := f.Type().Field(j) + // ok this is a bit weird but when we have an embedded nil struct + // calling FieldByName on a name which is present on this struct will panic + // Therefore we have to init them (non nil ptr), https://github.com/containers/podman/issues/14223 + if field.Anonymous && f.Field(j).Type().Kind() == reflect.Ptr { + f.Field(j).Set(reflect.New(f.Field(j).Type().Elem())) + } + } + // set the next struct field + f = f.FieldByName(fields[i]) + case reflect.Map: + rtype := f.Type().Elem() + if rtype.Kind() == reflect.Ptr { + rtype = rtype.Elem() + } + f = reflect.New(rtype) + case reflect.Func: + if f.Type().NumOut() != 1 { + // unsupported type return nothing + return nil, cobra.ShellCompDirectiveNoFileComp + } + f = reflect.New(f.Type().Out(0)) + default: + // unsupported type return nothing return nil, cobra.ShellCompDirectiveNoFileComp } - f = *val - - // set the next struct field - f = f.FieldByName(fields[i]) } return nil, cobra.ShellCompDirectiveNoFileComp } } -// getActualStructType take the value and check if it is a struct, +// actualReflectValue takes the value, // if it is pointer it will dereference it and when it is nil, -// it will create a new value from it to get the actual struct -// returns nil when type is not a struct -func getActualStructType(f reflect.Value) *reflect.Value { +// it will create a new value from it +func actualReflectValue(f reflect.Value) reflect.Value { // follow the pointer first if f.Kind() == reflect.Ptr { // if the pointer is nil we create a new value from the elements type - // this allows us to follow nil pointers and get the actual struct fields + // this allows us to follow nil pointers and get the actual type if f.IsNil() { f = reflect.New(f.Type().Elem()) } f = f.Elem() } - // we only support structs - if f.Kind() != reflect.Struct { - return nil - } - return &f + return f } // getStructFields reads all struct field names and method names and returns them. -func getStructFields(f reflect.Value, prefix string) []string { - var suggestions []string +func getStructFields(f reflect.Value, prefix string) []formatSuggestion { + var suggestions []formatSuggestion if f.IsValid() { suggestions = append(suggestions, getMethodNames(f, prefix)...) } - val := getActualStructType(f) - if val == nil { - // no struct return nothing to complete + f = actualReflectValue(f) + // we only support structs + if f.Kind() != reflect.Struct { return suggestions } - f = *val + var anonymous []formatSuggestion // loop over all field names for j := 0; j < f.NumField(); j++ { field := f.Type().Field(j) + // check if struct field is not exported, templates only use exported fields + // PkgPath is always empty for exported fields + if field.PkgPath != "" { + continue + } fname := field.Name suffix := "}}" kind := field.Type.Kind() @@ -1065,27 +1278,63 @@ func getStructFields(f reflect.Value, prefix string) []string { kind = field.Type.Elem().Kind() } // when we have a nested struct do not append braces instead append a dot - if kind == reflect.Struct { + if kind == reflect.Struct || kind == reflect.Map { suffix = "." } // if field is anonymous add the child fields as well if field.Anonymous { - suggestions = append(suggestions, getStructFields(f.Field(j), prefix)...) - } else if strings.HasPrefix(fname, prefix) { + anonymous = append(anonymous, getStructFields(f.Field(j), prefix)...) + } + if strings.HasPrefix(fname, prefix) { // add field name with suffix - suggestions = append(suggestions, fname+suffix) + suggestions = append(suggestions, formatSuggestion{fieldname: fname, suffix: suffix}) + } + } +outer: + for _, ano := range anonymous { + // we should only add anonymous child fields if they are not already present. + for _, sug := range suggestions { + if ano.fieldname == sug.fieldname { + continue outer + } } + suggestions = append(suggestions, ano) } return suggestions } -func getMethodNames(f reflect.Value, prefix string) []string { - suggestions := make([]string, 0, f.NumMethod()) +func getMethodNames(f reflect.Value, prefix string) []formatSuggestion { + suggestions := make([]formatSuggestion, 0, f.NumMethod()) for j := 0; j < f.NumMethod(); j++ { - fname := f.Type().Method(j).Name + method := f.Type().Method(j) + // in a template we can only run functions with one return value + if method.Func.Type().NumOut() != 1 { + continue + } + // when we have a nested struct do not append braces instead append a dot + kind := method.Func.Type().Out(0).Kind() + suffix := "}}" + if kind == reflect.Struct || kind == reflect.Map { + suffix = "." + } + // From a template users POV it is not important when the use a struct field or method. + // They only notice the difference when the function requires arguments. + // So lets be nice and let the user know that this method requires arguments via the help text. + // Note since this is actually a method on a type the first argument is always fix so we should skip it. + num := method.Func.Type().NumIn() - 1 + if num > 0 { + // everything after tab will the completion scripts show as help when enabled + // overwrite the suffix because it expects the args + suffix = "\tThis is a function and requires " + strconv.Itoa(num) + " argument" + if num > 1 { + // add plural s + suffix += "s" + } + } + fname := method.Name if strings.HasPrefix(fname, prefix) { // add method name with closing braces - suggestions = append(suggestions, fname+"}}") + suggestions = append(suggestions, formatSuggestion{fieldname: fname, suffix: suffix}) } } return suggestions @@ -1368,8 +1617,14 @@ func AutocompleteClone(cmd *cobra.Command, args []string, toComplete string) ([] } switch len(args) { case 0: + if cmd.Parent().Name() == "pod" { // needs to be " pod " to exclude 'podman' + return getPods(cmd, toComplete, completeDefault) + } return getContainers(cmd, toComplete, completeDefault) case 2: + if cmd.Parent().Name() == "pod" { + return nil, cobra.ShellCompDirectiveNoFileComp + } return getImages(cmd, toComplete) } return nil, cobra.ShellCompDirectiveNoFileComp |