diff options
Diffstat (limited to 'cmd')
63 files changed, 636 insertions, 869 deletions
diff --git a/cmd/podman/common/completion.go b/cmd/podman/common/completion.go index e07e28dab..07dcc4e6a 100644 --- a/cmd/podman/common/completion.go +++ b/cmd/podman/common/completion.go @@ -4,18 +4,25 @@ 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" "github.com/containers/podman/v4/cmd/podman/registry" "github.com/containers/podman/v4/libpod/define" + "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" ) @@ -24,6 +31,8 @@ var ( ChangeCmds = []string{"CMD", "ENTRYPOINT", "ENV", "EXPOSE", "LABEL", "ONBUILD", "STOPSIGNAL", "USER", "VOLUME", "WORKDIR"} // LogLevels supported by podman LogLevels = []string{"trace", "debug", "info", "warn", "warning", "error", "fatal", "panic"} + // ValidSaveFormats is the list of support podman save formats + ValidSaveFormats = []string{define.OCIManifestDir, define.OCIArchive, define.V2s2ManifestDir, define.V2s2Archive} ) type completeType int @@ -275,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 @@ -492,6 +585,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 @@ -511,8 +609,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. @@ -560,14 +682,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 @@ -591,7 +739,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 { @@ -796,8 +946,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 } @@ -836,10 +985,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 } @@ -960,9 +1125,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) { @@ -991,6 +1169,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 { @@ -999,61 +1183,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() @@ -1062,27 +1268,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 @@ -1091,11 +1333,21 @@ func getMethodNames(f reflect.Value, prefix string) []string { // AutocompleteEventFilter - Autocomplete event filter flag options. // -> "container=", "event=", "image=", "pod=", "volume=", "type=" func AutocompleteEventFilter(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + event := func(_ string) ([]string, cobra.ShellCompDirective) { + return []string{events.Attach.String(), events.AutoUpdate.String(), events.Checkpoint.String(), events.Cleanup.String(), + events.Commit.String(), events.Create.String(), events.Exec.String(), events.ExecDied.String(), + events.Exited.String(), events.Export.String(), events.Import.String(), events.Init.String(), events.Kill.String(), + events.LoadFromArchive.String(), events.Mount.String(), events.NetworkConnect.String(), + events.NetworkDisconnect.String(), events.Pause.String(), events.Prune.String(), events.Pull.String(), + events.Push.String(), events.Refresh.String(), events.Remove.String(), events.Rename.String(), + events.Renumber.String(), events.Restart.String(), events.Restore.String(), events.Save.String(), + events.Start.String(), events.Stop.String(), events.Sync.String(), events.Tag.String(), events.Unmount.String(), + events.Unpause.String(), events.Untag.String(), + }, cobra.ShellCompDirectiveNoFileComp + } eventTypes := func(_ string) ([]string, cobra.ShellCompDirective) { - return []string{"attach", "checkpoint", "cleanup", "commit", "connect", "create", "disconnect", "exec", - "exec_died", "exited", "export", "import", "init", "kill", "loadFromArchive", "mount", "pause", - "prune", "pull", "push", "refresh", "remove", "rename", "renumber", "restart", "restore", "save", - "start", "stop", "sync", "tag", "unmount", "unpause", "untag", + return []string{events.Container.String(), events.Image.String(), events.Network.String(), + events.Pod.String(), events.System.String(), events.Volume.String(), }, cobra.ShellCompDirectiveNoFileComp } kv := keyValueCompletion{ @@ -1103,7 +1355,7 @@ func AutocompleteEventFilter(cmd *cobra.Command, args []string, toComplete strin "image=": func(s string) ([]string, cobra.ShellCompDirective) { return getImages(cmd, s) }, "pod=": func(s string) ([]string, cobra.ShellCompDirective) { return getPods(cmd, s, completeDefault) }, "volume=": func(s string) ([]string, cobra.ShellCompDirective) { return getVolumes(cmd, s) }, - "event=": eventTypes, + "event=": event, "type=": eventTypes, } return completeKeyValues(toComplete, kv) @@ -1130,9 +1382,8 @@ func AutocompleteImageSort(cmd *cobra.Command, args []string, toComplete string) } // AutocompleteInspectType - Autocomplete inspect type options. -// -> "container", "image", "all" func AutocompleteInspectType(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - types := []string{"container", "image", "all"} + types := []string{AllType, ContainerType, ImageType, NetworkType, PodType, VolumeType} return types, cobra.ShellCompDirectiveNoFileComp } @@ -1182,10 +1433,8 @@ func AutocompletePsSort(cmd *cobra.Command, args []string, toComplete string) ([ } // AutocompleteImageSaveFormat - Autocomplete image save format options. -// -> "oci-archive", "oci-dir", "docker-dir" func AutocompleteImageSaveFormat(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - formats := []string{"oci-archive", "oci-dir", "docker-dir"} - return formats, cobra.ShellCompDirectiveNoFileComp + return ValidSaveFormats, cobra.ShellCompDirectiveNoFileComp } // AutocompleteWaitCondition - Autocomplete wait condition options. @@ -1198,21 +1447,21 @@ func AutocompleteWaitCondition(cmd *cobra.Command, args []string, toComplete str // AutocompleteCgroupManager - Autocomplete cgroup manager options. // -> "cgroupfs", "systemd" func AutocompleteCgroupManager(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - types := []string{"cgroupfs", "systemd"} + types := []string{config.CgroupfsCgroupsManager, config.SystemdCgroupsManager} return types, cobra.ShellCompDirectiveNoFileComp } // AutocompleteEventBackend - Autocomplete event backend options. // -> "file", "journald", "none" func AutocompleteEventBackend(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - types := []string{"file", "journald", "none"} + types := []string{events.LogFile.String(), events.Journald.String(), events.Null.String()} return types, cobra.ShellCompDirectiveNoFileComp } // AutocompleteNetworkBackend - Autocomplete network backend options. // -> "cni", "netavark" func AutocompleteNetworkBackend(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - types := []string{"cni", "netavark"} + types := []string{string(types.CNI), string(types.Netavark)} return types, cobra.ShellCompDirectiveNoFileComp } @@ -1225,7 +1474,7 @@ func AutocompleteLogLevel(cmd *cobra.Command, args []string, toComplete string) // AutocompleteSDNotify - Autocomplete sdnotify options. // -> "container", "conmon", "ignore" func AutocompleteSDNotify(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - types := []string{"container", "conmon", "ignore"} + types := []string{define.SdNotifyModeContainer, define.SdNotifyModeContainer, define.SdNotifyModeIgnore} return types, cobra.ShellCompDirectiveNoFileComp } diff --git a/cmd/podman/common/completion_test.go b/cmd/podman/common/completion_test.go index 13f45a662..ae23b02e2 100644 --- a/cmd/podman/common/completion_test.go +++ b/cmd/podman/common/completion_test.go @@ -14,11 +14,29 @@ type Car struct { HP *int Displacement int } - Extras map[string]string + Extras map[string]Extra + // also ensure it will work with pointers + Extras2 map[string]*Extra +} + +type Extra struct { + Name1 string + Name2 string } type Anonymous struct { Hello string + // The name should match the testStruct Name below. This is used to make + // sure the logic uses the actual struct fields before the embedded ones. + Name struct { + Suffix string + Prefix string + } +} + +// The name should match the testStruct Age name below. +func (a Anonymous) Age() int { + return 0 } func (c Car) Type() string { @@ -31,6 +49,20 @@ func (c *Car) Color() string { return "" } +// This is for reflect testing required. +// nolint:unused +func (c Car) internal() int { + return 0 +} + +func (c Car) TwoOut() (string, string) { + return "", "" +} + +func (c Car) Struct() Car { + return Car{} +} + func TestAutocompleteFormat(t *testing.T) { testStruct := struct { Name string @@ -38,10 +70,10 @@ func TestAutocompleteFormat(t *testing.T) { Car *Car Car2 *Car *Anonymous + private int }{} testStruct.Car = &Car{} - testStruct.Car.Extras = map[string]string{"test": "1"} tests := []struct { name string @@ -76,17 +108,17 @@ func TestAutocompleteFormat(t *testing.T) { { "invalid completion", "{{ ..", - nil, + []string{}, }, { "fist level struct field name", "{{.", - []string{"{{.Name}}", "{{.Age}}", "{{.Car.", "{{.Car2.", "{{.Hello}}"}, + []string{"{{.Name}}", "{{.Age}}", "{{.Car.", "{{.Car2.", "{{.Anonymous.", "{{.Hello}}"}, }, { "fist level struct field name", "{{ .", - []string{"{{ .Name}}", "{{ .Age}}", "{{ .Car.", "{{ .Car2.", "{{ .Hello}}"}, + []string{"{{ .Name}}", "{{ .Age}}", "{{ .Car.", "{{ .Car2.", "{{ .Anonymous.", "{{ .Hello}}"}, }, { "fist level struct field name", @@ -96,7 +128,7 @@ func TestAutocompleteFormat(t *testing.T) { { "second level struct field name", "{{ .Car.", - []string{"{{ .Car.Color}}", "{{ .Car.Type}}", "{{ .Car.Brand}}", "{{ .Car.Stats.", "{{ .Car.Extras}}"}, + []string{"{{ .Car.Color}}", "{{ .Car.Struct.", "{{ .Car.Type}}", "{{ .Car.Brand}}", "{{ .Car.Stats.", "{{ .Car.Extras.", "{{ .Car.Extras2."}, }, { "second level struct field name", @@ -106,7 +138,7 @@ func TestAutocompleteFormat(t *testing.T) { { "second level nil struct field name", "{{ .Car2.", - []string{"{{ .Car2.Color}}", "{{ .Car2.Type}}", "{{ .Car2.Brand}}", "{{ .Car2.Stats.", "{{ .Car2.Extras}}"}, + []string{"{{ .Car2.Color}}", "{{ .Car2.Struct.", "{{ .Car2.Type}}", "{{ .Car2.Brand}}", "{{ .Car2.Stats.", "{{ .Car2.Extras.", "{{ .Car2.Extras2."}, }, { "three level struct field name", @@ -126,28 +158,44 @@ func TestAutocompleteFormat(t *testing.T) { { "invalid field name", "{{ .Ca.B", - nil, + []string{}, }, { "map key names don't work", "{{ .Car.Extras.", - nil, + []string{}, + }, + { + "map values work", + "{{ .Car.Extras.somekey.", + []string{"{{ .Car.Extras.somekey.Name1}}", "{{ .Car.Extras.somekey.Name2}}"}, + }, + { + "map values work with ptr", + "{{ .Car.Extras2.somekey.", + []string{"{{ .Car.Extras2.somekey.Name1}}", "{{ .Car.Extras2.somekey.Name2}}"}, }, { "two variables struct field name", "{{ .Car.Brand }} {{ .Car.", - []string{"{{ .Car.Brand }} {{ .Car.Color}}", "{{ .Car.Brand }} {{ .Car.Type}}", "{{ .Car.Brand }} {{ .Car.Brand}}", - "{{ .Car.Brand }} {{ .Car.Stats.", "{{ .Car.Brand }} {{ .Car.Extras}}"}, + []string{"{{ .Car.Brand }} {{ .Car.Color}}", "{{ .Car.Brand }} {{ .Car.Struct.", "{{ .Car.Brand }} {{ .Car.Type}}", + "{{ .Car.Brand }} {{ .Car.Brand}}", "{{ .Car.Brand }} {{ .Car.Stats.", "{{ .Car.Brand }} {{ .Car.Extras.", + "{{ .Car.Brand }} {{ .Car.Extras2."}, }, { "only dot without variable", ".", nil, }, + { + "access embedded nil struct field", + "{{.Hello.", + []string{}, + }, } for _, test := range tests { - completion, directive := common.AutocompleteFormat(testStruct)(nil, nil, test.toComplete) + completion, directive := common.AutocompleteFormat(&testStruct)(nil, nil, test.toComplete) // directive should always be greater than ShellCompDirectiveNoFileComp assert.GreaterOrEqual(t, directive, cobra.ShellCompDirectiveNoFileComp, "unexpected ShellCompDirective") assert.Equal(t, test.expected, completion, test.name) diff --git a/cmd/podman/common/create.go b/cmd/podman/common/create.go index d28becc8a..f89035be3 100644 --- a/cmd/podman/common/create.go +++ b/cmd/podman/common/create.go @@ -12,7 +12,7 @@ import ( "github.com/spf13/cobra" ) -const sizeWithUnitFormat = "(format: `<number>[<unit>]`, where unit = b (bytes), k (kilobytes), m (megabytes), or g (gigabytes))" +const sizeWithUnitFormat = "(format: `<number>[<unit>]`, where unit = b (bytes), k (kibibytes), m (mebibytes), or g (gibibytes))" var containerConfig = registry.PodmanConfig() @@ -255,9 +255,8 @@ func DefineCreateFlags(cmd *cobra.Command, cf *entities.ContainerCreateOptions, _ = cmd.RegisterFlagCompletionFunc(hostUserFlagName, completion.AutocompleteNone) imageVolumeFlagName := "image-volume" - createFlags.StringVar( - &cf.ImageVolume, - imageVolumeFlagName, DefaultImageVolume, + createFlags.String( + imageVolumeFlagName, containerConfig.Engine.ImageVolumeMode, `Tells podman how to handle the builtin image volumes ("bind"|"tmpfs"|"ignore")`, ) _ = cmd.RegisterFlagCompletionFunc(imageVolumeFlagName, AutocompleteImageVolume) diff --git a/cmd/podman/common/create_opts.go b/cmd/podman/common/create_opts.go index c40d1ea51..ad535ff59 100644 --- a/cmd/podman/common/create_opts.go +++ b/cmd/podman/common/create_opts.go @@ -1,472 +1,9 @@ package common import ( - "fmt" - "net" - "os" - "path/filepath" - "strconv" - "strings" - - "github.com/containers/common/libnetwork/types" - "github.com/containers/common/pkg/cgroups" - "github.com/containers/common/pkg/config" "github.com/containers/podman/v4/cmd/podman/registry" - "github.com/containers/podman/v4/libpod/define" - "github.com/containers/podman/v4/pkg/api/handlers" - "github.com/containers/podman/v4/pkg/domain/entities" - "github.com/containers/podman/v4/pkg/rootless" - "github.com/containers/podman/v4/pkg/specgen" - "github.com/docker/docker/api/types/mount" - "github.com/pkg/errors" ) -func stringMaptoArray(m map[string]string) []string { - a := make([]string, 0, len(m)) - for k, v := range m { - a = append(a, fmt.Sprintf("%s=%s", k, v)) - } - return a -} - -// ContainerCreateToContainerCLIOpts converts a compat input struct to cliopts so it can be converted to -// a specgen spec. -func ContainerCreateToContainerCLIOpts(cc handlers.CreateContainerConfig, rtc *config.Config) (*entities.ContainerCreateOptions, []string, error) { - var ( - capAdd []string - cappDrop []string - entrypoint *string - init bool - specPorts []types.PortMapping - ) - - if cc.HostConfig.Init != nil { - init = *cc.HostConfig.Init - } - - // Iterate devices and convert back to string - devices := make([]string, 0, len(cc.HostConfig.Devices)) - for _, dev := range cc.HostConfig.Devices { - devices = append(devices, fmt.Sprintf("%s:%s:%s", dev.PathOnHost, dev.PathInContainer, dev.CgroupPermissions)) - } - - // iterate blkreaddevicebps - readBps := make([]string, 0, len(cc.HostConfig.BlkioDeviceReadBps)) - for _, dev := range cc.HostConfig.BlkioDeviceReadBps { - readBps = append(readBps, dev.String()) - } - - // iterate blkreaddeviceiops - readIops := make([]string, 0, len(cc.HostConfig.BlkioDeviceReadIOps)) - for _, dev := range cc.HostConfig.BlkioDeviceReadIOps { - readIops = append(readIops, dev.String()) - } - - // iterate blkwritedevicebps - writeBps := make([]string, 0, len(cc.HostConfig.BlkioDeviceWriteBps)) - for _, dev := range cc.HostConfig.BlkioDeviceWriteBps { - writeBps = append(writeBps, dev.String()) - } - - // iterate blkwritedeviceiops - writeIops := make([]string, 0, len(cc.HostConfig.BlkioDeviceWriteIOps)) - for _, dev := range cc.HostConfig.BlkioDeviceWriteIOps { - writeIops = append(writeIops, dev.String()) - } - - // entrypoint - // can be a string or slice. if it is a slice, we need to - // marshall it to json; otherwise it should just be the string - // value - if len(cc.Config.Entrypoint) > 0 { - entrypoint = &cc.Config.Entrypoint[0] - if len(cc.Config.Entrypoint) > 1 { - b, err := json.Marshal(cc.Config.Entrypoint) - if err != nil { - return nil, nil, err - } - var jsonString = string(b) - entrypoint = &jsonString - } - } - - // expose ports - expose := make([]string, 0, len(cc.Config.ExposedPorts)) - for p := range cc.Config.ExposedPorts { - expose = append(expose, fmt.Sprintf("%s/%s", p.Port(), p.Proto())) - } - - // mounts type=tmpfs/bind,source=...,target=...=,opt=val - volSources := make(map[string]bool) - volDestinations := make(map[string]bool) - mounts := make([]string, 0, len(cc.HostConfig.Mounts)) - var builder strings.Builder - for _, m := range cc.HostConfig.Mounts { - addField(&builder, "type", string(m.Type)) - addField(&builder, "source", m.Source) - addField(&builder, "target", m.Target) - - // Store source/dest so we don't add duplicates if a volume is - // also mentioned in cc.Volumes. - // Which Docker Compose v2.0 does, for unclear reasons... - volSources[m.Source] = true - volDestinations[m.Target] = true - - if m.ReadOnly { - addField(&builder, "ro", "true") - } - addField(&builder, "consistency", string(m.Consistency)) - // Map any specialized mount options that intersect between *Options and cli options - switch m.Type { - case mount.TypeBind: - if m.BindOptions != nil { - addField(&builder, "bind-propagation", string(m.BindOptions.Propagation)) - addField(&builder, "bind-nonrecursive", strconv.FormatBool(m.BindOptions.NonRecursive)) - } - case mount.TypeTmpfs: - if m.TmpfsOptions != nil { - addField(&builder, "tmpfs-size", strconv.FormatInt(m.TmpfsOptions.SizeBytes, 10)) - addField(&builder, "tmpfs-mode", strconv.FormatUint(uint64(m.TmpfsOptions.Mode), 8)) - } - case mount.TypeVolume: - // All current VolumeOpts are handled above - // See vendor/github.com/containers/common/pkg/parse/parse.go:ValidateVolumeOpts() - } - mounts = append(mounts, builder.String()) - builder.Reset() - } - - // dns - dns := make([]net.IP, 0, len(cc.HostConfig.DNS)) - for _, d := range cc.HostConfig.DNS { - dns = append(dns, net.ParseIP(d)) - } - - // publish - for port, pbs := range cc.HostConfig.PortBindings { - for _, pb := range pbs { - var hostport int - var err error - if pb.HostPort != "" { - hostport, err = strconv.Atoi(pb.HostPort) - } - if err != nil { - return nil, nil, err - } - tmpPort := types.PortMapping{ - HostIP: pb.HostIP, - ContainerPort: uint16(port.Int()), - HostPort: uint16(hostport), - Range: 0, - Protocol: port.Proto(), - } - specPorts = append(specPorts, tmpPort) - } - } - - // special case for NetworkMode, the podman default is slirp4netns for - // rootless but for better docker compat we want bridge. - netmode := string(cc.HostConfig.NetworkMode) - if netmode == "" || netmode == "default" { - netmode = "bridge" - } - nsmode, networks, netOpts, err := specgen.ParseNetworkFlag([]string{netmode}) - if err != nil { - return nil, nil, err - } - - // network - // Note: we cannot emulate compat exactly here. we only allow specifics of networks to be - // defined when there is only one network. - netInfo := entities.NetOptions{ - AddHosts: cc.HostConfig.ExtraHosts, - DNSOptions: cc.HostConfig.DNSOptions, - DNSSearch: cc.HostConfig.DNSSearch, - DNSServers: dns, - Network: nsmode, - PublishPorts: specPorts, - NetworkOptions: netOpts, - NoHosts: rtc.Containers.NoHosts, - } - - // network names - switch { - case len(cc.NetworkingConfig.EndpointsConfig) > 0: - endpointsConfig := cc.NetworkingConfig.EndpointsConfig - networks := make(map[string]types.PerNetworkOptions, len(endpointsConfig)) - for netName, endpoint := range endpointsConfig { - netOpts := types.PerNetworkOptions{} - if endpoint != nil { - netOpts.Aliases = endpoint.Aliases - - // if IP address is provided - if len(endpoint.IPAddress) > 0 { - staticIP := net.ParseIP(endpoint.IPAddress) - if staticIP == nil { - return nil, nil, errors.Errorf("failed to parse the ip address %q", endpoint.IPAddress) - } - netOpts.StaticIPs = append(netOpts.StaticIPs, staticIP) - } - - if endpoint.IPAMConfig != nil { - // if IPAMConfig.IPv4Address is provided - if len(endpoint.IPAMConfig.IPv4Address) > 0 { - staticIP := net.ParseIP(endpoint.IPAMConfig.IPv4Address) - if staticIP == nil { - return nil, nil, errors.Errorf("failed to parse the ipv4 address %q", endpoint.IPAMConfig.IPv4Address) - } - netOpts.StaticIPs = append(netOpts.StaticIPs, staticIP) - } - // if IPAMConfig.IPv6Address is provided - if len(endpoint.IPAMConfig.IPv6Address) > 0 { - staticIP := net.ParseIP(endpoint.IPAMConfig.IPv6Address) - if staticIP == nil { - return nil, nil, errors.Errorf("failed to parse the ipv6 address %q", endpoint.IPAMConfig.IPv6Address) - } - netOpts.StaticIPs = append(netOpts.StaticIPs, staticIP) - } - } - // If MAC address is provided - if len(endpoint.MacAddress) > 0 { - staticMac, err := net.ParseMAC(endpoint.MacAddress) - if err != nil { - return nil, nil, errors.Errorf("failed to parse the mac address %q", endpoint.MacAddress) - } - netOpts.StaticMAC = types.HardwareAddr(staticMac) - } - } - - networks[netName] = netOpts - } - - netInfo.Networks = networks - case len(cc.HostConfig.NetworkMode) > 0: - netInfo.Networks = networks - } - - parsedTmp := make([]string, 0, len(cc.HostConfig.Tmpfs)) - for path, options := range cc.HostConfig.Tmpfs { - finalString := path - if options != "" { - finalString += ":" + options - } - parsedTmp = append(parsedTmp, finalString) - } - - // Note: several options here are marked as "don't need". this is based - // on speculation by Matt and I. We think that these come into play later - // like with start. We believe this is just a difference in podman/compat - cliOpts := entities.ContainerCreateOptions{ - // Attach: nil, // don't need? - Authfile: "", - CapAdd: append(capAdd, cc.HostConfig.CapAdd...), - CapDrop: append(cappDrop, cc.HostConfig.CapDrop...), - CgroupParent: cc.HostConfig.CgroupParent, - CIDFile: cc.HostConfig.ContainerIDFile, - CPUPeriod: uint64(cc.HostConfig.CPUPeriod), - CPUQuota: cc.HostConfig.CPUQuota, - CPURTPeriod: uint64(cc.HostConfig.CPURealtimePeriod), - CPURTRuntime: cc.HostConfig.CPURealtimeRuntime, - CPUShares: uint64(cc.HostConfig.CPUShares), - // CPUS: 0, // don't need? - CPUSetCPUs: cc.HostConfig.CpusetCpus, - CPUSetMems: cc.HostConfig.CpusetMems, - // Detach: false, // don't need - // DetachKeys: "", // don't need - Devices: devices, - DeviceCgroupRule: nil, - DeviceReadBPs: readBps, - DeviceReadIOPs: readIops, - DeviceWriteBPs: writeBps, - DeviceWriteIOPs: writeIops, - Entrypoint: entrypoint, - Env: cc.Config.Env, - Expose: expose, - GroupAdd: cc.HostConfig.GroupAdd, - Hostname: cc.Config.Hostname, - ImageVolume: "bind", - Init: init, - Interactive: cc.Config.OpenStdin, - IPC: string(cc.HostConfig.IpcMode), - Label: stringMaptoArray(cc.Config.Labels), - LogDriver: cc.HostConfig.LogConfig.Type, - LogOptions: stringMaptoArray(cc.HostConfig.LogConfig.Config), - Name: cc.Name, - OOMScoreAdj: &cc.HostConfig.OomScoreAdj, - Arch: "", - OS: "", - Variant: "", - PID: string(cc.HostConfig.PidMode), - PIDsLimit: cc.HostConfig.PidsLimit, - Privileged: cc.HostConfig.Privileged, - PublishAll: cc.HostConfig.PublishAllPorts, - Quiet: false, - ReadOnly: cc.HostConfig.ReadonlyRootfs, - ReadOnlyTmpFS: true, // podman default - Rm: cc.HostConfig.AutoRemove, - SecurityOpt: cc.HostConfig.SecurityOpt, - StopSignal: cc.Config.StopSignal, - StorageOpts: stringMaptoArray(cc.HostConfig.StorageOpt), - Sysctl: stringMaptoArray(cc.HostConfig.Sysctls), - Systemd: "true", // podman default - TmpFS: parsedTmp, - TTY: cc.Config.Tty, - UnsetEnv: cc.UnsetEnv, - UnsetEnvAll: cc.UnsetEnvAll, - User: cc.Config.User, - UserNS: string(cc.HostConfig.UsernsMode), - UTS: string(cc.HostConfig.UTSMode), - Mount: mounts, - VolumesFrom: cc.HostConfig.VolumesFrom, - Workdir: cc.Config.WorkingDir, - Net: &netInfo, - HealthInterval: define.DefaultHealthCheckInterval, - HealthRetries: define.DefaultHealthCheckRetries, - HealthTimeout: define.DefaultHealthCheckTimeout, - HealthStartPeriod: define.DefaultHealthCheckStartPeriod, - } - if !rootless.IsRootless() { - var ulimits []string - if len(cc.HostConfig.Ulimits) > 0 { - for _, ul := range cc.HostConfig.Ulimits { - ulimits = append(ulimits, ul.String()) - } - cliOpts.Ulimit = ulimits - } - } - if cc.HostConfig.Resources.NanoCPUs > 0 { - if cliOpts.CPUPeriod != 0 || cliOpts.CPUQuota != 0 { - return nil, nil, errors.Errorf("NanoCpus conflicts with CpuPeriod and CpuQuota") - } - cliOpts.CPUPeriod = 100000 - cliOpts.CPUQuota = cc.HostConfig.Resources.NanoCPUs / 10000 - } - - // volumes - for _, vol := range cc.HostConfig.Binds { - cliOpts.Volume = append(cliOpts.Volume, vol) - // Extract the destination so we don't add duplicate mounts in - // the volumes phase. - splitVol := specgen.SplitVolumeString(vol) - switch len(splitVol) { - case 1: - volDestinations[vol] = true - default: - volSources[splitVol[0]] = true - volDestinations[splitVol[1]] = true - } - } - // Anonymous volumes are added differently from other volumes, in their - // own special field, for reasons known only to Docker. Still use the - // format of `-v` so we can just append them in there. - // Unfortunately, these may be duplicates of existing mounts in Binds. - // So... We need to catch that. - // This also handles volumes duplicated between cc.HostConfig.Mounts and - // cc.Volumes, as seen in compose v2.0. - for vol := range cc.Volumes { - if _, ok := volDestinations[filepath.Clean(vol)]; ok { - continue - } - cliOpts.Volume = append(cliOpts.Volume, vol) - } - // Make mount points for compat volumes - for vol := range volSources { - // This might be a named volume. - // Assume it is if it's not an absolute path. - if !filepath.IsAbs(vol) { - continue - } - // If volume already exists, there is nothing to do - if _, err := os.Stat(vol); err == nil { - continue - } - if err := os.MkdirAll(vol, 0755); err != nil { - if !os.IsExist(err) { - return nil, nil, errors.Wrapf(err, "error making volume mountpoint for volume %s", vol) - } - } - } - if len(cc.HostConfig.BlkioWeightDevice) > 0 { - devices := make([]string, 0, len(cc.HostConfig.BlkioWeightDevice)) - for _, d := range cc.HostConfig.BlkioWeightDevice { - devices = append(devices, d.String()) - } - cliOpts.BlkIOWeightDevice = devices - } - if cc.HostConfig.BlkioWeight > 0 { - cliOpts.BlkIOWeight = strconv.Itoa(int(cc.HostConfig.BlkioWeight)) - } - - if cc.HostConfig.Memory > 0 { - cliOpts.Memory = strconv.Itoa(int(cc.HostConfig.Memory)) - } - - if cc.HostConfig.MemoryReservation > 0 { - cliOpts.MemoryReservation = strconv.Itoa(int(cc.HostConfig.MemoryReservation)) - } - - cgroupsv2, err := cgroups.IsCgroup2UnifiedMode() - if err != nil { - return nil, nil, err - } - if cc.HostConfig.MemorySwap > 0 && (!rootless.IsRootless() || (rootless.IsRootless() && cgroupsv2)) { - cliOpts.MemorySwap = strconv.Itoa(int(cc.HostConfig.MemorySwap)) - } - - if cc.Config.StopTimeout != nil { - cliOpts.StopTimeout = uint(*cc.Config.StopTimeout) - } - - if cc.HostConfig.ShmSize > 0 { - cliOpts.ShmSize = strconv.Itoa(int(cc.HostConfig.ShmSize)) - } - - if len(cc.HostConfig.RestartPolicy.Name) > 0 { - policy := cc.HostConfig.RestartPolicy.Name - // only add restart count on failure - if cc.HostConfig.RestartPolicy.IsOnFailure() { - policy += fmt.Sprintf(":%d", cc.HostConfig.RestartPolicy.MaximumRetryCount) - } - cliOpts.Restart = policy - } - - if cc.HostConfig.MemorySwappiness != nil && (!rootless.IsRootless() || rootless.IsRootless() && cgroupsv2 && rtc.Engine.CgroupManager == "systemd") { - cliOpts.MemorySwappiness = *cc.HostConfig.MemorySwappiness - } else { - cliOpts.MemorySwappiness = -1 - } - if cc.HostConfig.OomKillDisable != nil { - cliOpts.OOMKillDisable = *cc.HostConfig.OomKillDisable - } - if cc.Config.Healthcheck != nil { - finCmd := "" - for _, str := range cc.Config.Healthcheck.Test { - finCmd = finCmd + str + " " - } - if len(finCmd) > 1 { - finCmd = finCmd[:len(finCmd)-1] - } - cliOpts.HealthCmd = finCmd - if cc.Config.Healthcheck.Interval > 0 { - cliOpts.HealthInterval = cc.Config.Healthcheck.Interval.String() - } - if cc.Config.Healthcheck.Retries > 0 { - cliOpts.HealthRetries = uint(cc.Config.Healthcheck.Retries) - } - if cc.Config.Healthcheck.StartPeriod > 0 { - cliOpts.HealthStartPeriod = cc.Config.Healthcheck.StartPeriod.String() - } - if cc.Config.Healthcheck.Timeout > 0 { - cliOpts.HealthTimeout = cc.Config.Healthcheck.Timeout.String() - } - } - - // specgen assumes the image name is arg[0] - cmd := []string{cc.Config.Image} - cmd = append(cmd, cc.Config.Cmd...) - return &cliOpts, cmd, nil -} - func ulimits() []string { if !registry.IsRemote() { return containerConfig.Ulimits() @@ -536,17 +73,3 @@ func LogDriver() string { } return "" } - -// addField is a helper function to populate mount options -func addField(b *strings.Builder, name string, value string) { - if value == "" { - return - } - - if b.Len() > 0 { - b.WriteRune(',') - } - b.WriteString(name) - b.WriteRune('=') - b.WriteString(value) -} diff --git a/cmd/podman/common/create_test.go b/cmd/podman/common/create_test.go index ab41f81ad..80e6cbf54 100644 --- a/cmd/podman/common/create_test.go +++ b/cmd/podman/common/create_test.go @@ -28,8 +28,8 @@ func TestPodOptions(t *testing.T) { for j := 0; j < cc.NumField(); j++ { containerField := cc.FieldByIndex([]int{j}) containerType := reflect.TypeOf(exampleOptions).Field(j) - tagPod := strings.Split(string(podType.Tag.Get("json")), ",")[0] - tagContainer := strings.Split(string(containerType.Tag.Get("json")), ",")[0] + tagPod := strings.Split(podType.Tag.Get("json"), ",")[0] + tagContainer := strings.Split(containerType.Tag.Get("json"), ",")[0] if tagPod == tagContainer && (tagPod != "" && tagContainer != "") { areEqual := true if containerField.Kind() == podField.Kind() { diff --git a/cmd/podman/common/default.go b/cmd/podman/common/default.go index 7caec50ff..6f78d3d29 100644 --- a/cmd/podman/common/default.go +++ b/cmd/podman/common/default.go @@ -5,9 +5,6 @@ import ( ) var ( - - // DefaultImageVolume default value - DefaultImageVolume = "bind" // Pull in configured json library json = registry.JSONLibrary() ) diff --git a/cmd/podman/common/inspect.go b/cmd/podman/common/inspect.go new file mode 100644 index 000000000..12a5af5a9 --- /dev/null +++ b/cmd/podman/common/inspect.go @@ -0,0 +1,16 @@ +package common + +const ( + // AllType can be of type ImageType or ContainerType. + AllType = "all" + // ContainerType is the container type. + ContainerType = "container" + // ImageType is the image type. + ImageType = "image" + // NetworkType is the network type + NetworkType = "network" + // PodType is the pod type. + PodType = "pod" + // VolumeType is the volume type + VolumeType = "volume" +) diff --git a/cmd/podman/containers/checkpoint.go b/cmd/podman/containers/checkpoint.go index 40d689c4d..e0891f7a1 100644 --- a/cmd/podman/containers/checkpoint.go +++ b/cmd/podman/containers/checkpoint.go @@ -31,7 +31,7 @@ var ( Long: checkpointDescription, RunE: checkpoint, Args: func(cmd *cobra.Command, args []string) error { - return validate.CheckAllLatestAndCIDFile(cmd, args, false, false) + return validate.CheckAllLatestAndIDFile(cmd, args, false, "") }, ValidArgsFunction: common.AutocompleteContainersRunning, Example: `podman container checkpoint --keep ctrID diff --git a/cmd/podman/containers/cleanup.go b/cmd/podman/containers/cleanup.go index aa2734607..a63e413fe 100644 --- a/cmd/podman/containers/cleanup.go +++ b/cmd/podman/containers/cleanup.go @@ -27,7 +27,7 @@ var ( Long: cleanupDescription, RunE: cleanup, Args: func(cmd *cobra.Command, args []string) error { - return validate.CheckAllLatestAndCIDFile(cmd, args, false, false) + return validate.CheckAllLatestAndIDFile(cmd, args, false, "") }, ValidArgsFunction: common.AutocompleteContainersExited, Example: `podman container cleanup --latest diff --git a/cmd/podman/containers/cp.go b/cmd/podman/containers/cp.go index eb18dfce4..bf591cf65 100644 --- a/cmd/podman/containers/cp.go +++ b/cmd/podman/containers/cp.go @@ -55,10 +55,13 @@ var ( func cpFlags(cmd *cobra.Command) { flags := cmd.Flags() - flags.BoolVar(&cpOpts.Extract, "extract", false, "Deprecated...") - flags.BoolVar(&cpOpts.Pause, "pause", true, "Deprecated") + flags.BoolVar(&cpOpts.OverwriteDirNonDir, "overwrite", false, "Allow to overwrite directories with non-directories and vice versa") flags.BoolVarP(&chown, "archive", "a", true, `Chown copied files to the primary uid/gid of the destination container.`) + + // Deprecated flags (both are NOPs): exist for backwards compat + flags.BoolVar(&cpOpts.Extract, "extract", false, "Deprecated...") _ = flags.MarkHidden("extract") + flags.BoolVar(&cpOpts.Pause, "pause", true, "Deprecated") _ = flags.MarkHidden("pause") } @@ -175,7 +178,7 @@ func copyContainerToContainer(sourceContainer string, sourcePath string, destCon destContainerCopy := func() error { defer reader.Close() - copyOptions := entities.CopyOptions{Chown: chown} + copyOptions := entities.CopyOptions{Chown: chown, NoOverwriteDirNonDir: !cpOpts.OverwriteDirNonDir} if (!sourceContainerInfo.IsDir && !destContainerInfo.IsDir) || destResolvedToParentDir { // If we're having a file-to-file copy, make sure to // rename accordingly. @@ -294,9 +297,11 @@ func copyFromContainer(container string, containerPath string, hostPath string) } putOptions := buildahCopiah.PutOptions{ - ChownDirs: &idPair, - ChownFiles: &idPair, - IgnoreDevices: true, + ChownDirs: &idPair, + ChownFiles: &idPair, + IgnoreDevices: true, + NoOverwriteDirNonDir: !cpOpts.OverwriteDirNonDir, + NoOverwriteNonDirDir: !cpOpts.OverwriteDirNonDir, } if (!containerInfo.IsDir && !hostInfo.IsDir) || resolvedToHostParentDir { // If we're having a file-to-file copy, make sure to @@ -429,7 +434,7 @@ func copyToContainer(container string, containerPath string, hostPath string) er target = filepath.Dir(target) } - copyFunc, err := registry.ContainerEngine().ContainerCopyFromArchive(registry.GetContext(), container, target, reader, entities.CopyOptions{Chown: chown}) + copyFunc, err := registry.ContainerEngine().ContainerCopyFromArchive(registry.GetContext(), container, target, reader, entities.CopyOptions{Chown: chown, NoOverwriteDirNonDir: !cpOpts.OverwriteDirNonDir}) if err != nil { return err } diff --git a/cmd/podman/containers/create.go b/cmd/podman/containers/create.go index 29e138e30..0a513c606 100644 --- a/cmd/podman/containers/create.go +++ b/cmd/podman/containers/create.go @@ -9,6 +9,7 @@ import ( "github.com/containers/common/pkg/completion" "github.com/containers/common/pkg/config" + cutil "github.com/containers/common/pkg/util" "github.com/containers/image/v5/transports/alltransports" "github.com/containers/image/v5/types" "github.com/containers/podman/v4/cmd/podman/common" @@ -101,28 +102,37 @@ func init() { createFlags(containerCreateCommand) } -func create(cmd *cobra.Command, args []string) error { - var ( - err error - ) +func commonFlags(cmd *cobra.Command) error { + var err error flags := cmd.Flags() cliVals.Net, err = common.NetFlagsToNetOptions(nil, *flags) if err != nil { return err } + if cmd.Flags().Changed("image-volume") { + cliVals.ImageVolume = cmd.Flag("image-volume").Value.String() + } + return nil +} + +func create(cmd *cobra.Command, args []string) error { + if err := commonFlags(cmd); err != nil { + return err + } + // Check if initctr is used with --pod and the value is correct if initctr := InitContainerType; cmd.Flags().Changed("init-ctr") { if !cmd.Flags().Changed("pod") { return errors.New("must specify pod value with init-ctr") } - if !util.StringInSlice(initctr, []string{define.AlwaysInitContainer, define.OneShotInitContainer}) { + if !cutil.StringInSlice(initctr, []string{define.AlwaysInitContainer, define.OneShotInitContainer}) { return errors.Errorf("init-ctr value must be '%s' or '%s'", define.AlwaysInitContainer, define.OneShotInitContainer) } cliVals.InitContainerType = initctr } - cliVals, err = CreateInit(cmd, cliVals, false) + cliVals, err := CreateInit(cmd, cliVals, false) if err != nil { return err } diff --git a/cmd/podman/containers/diff.go b/cmd/podman/containers/diff.go index e1a8ea729..15d3a3eff 100644 --- a/cmd/podman/containers/diff.go +++ b/cmd/podman/containers/diff.go @@ -32,13 +32,9 @@ func init() { Parent: containerCmd, }) - diffOpts = &entities.DiffOptions{} + diffOpts = new(entities.DiffOptions) flags := diffCmd.Flags() - // FIXME: Why does this exists? It is not used anywhere. - flags.BoolVar(&diffOpts.Archive, "archive", true, "Save the diff as a tar archive") - _ = flags.MarkHidden("archive") - formatFlagName := "format" flags.StringVar(&diffOpts.Format, formatFlagName, "", "Change the output format (json)") _ = diffCmd.RegisterFlagCompletionFunc(formatFlagName, common.AutocompleteFormat(nil)) diff --git a/cmd/podman/containers/init.go b/cmd/podman/containers/init.go index 7336a2332..649cdf1c9 100644 --- a/cmd/podman/containers/init.go +++ b/cmd/podman/containers/init.go @@ -21,7 +21,7 @@ var ( Long: initDescription, RunE: initContainer, Args: func(cmd *cobra.Command, args []string) error { - return validate.CheckAllLatestAndCIDFile(cmd, args, false, false) + return validate.CheckAllLatestAndIDFile(cmd, args, false, "") }, ValidArgsFunction: common.AutocompleteContainersCreated, Example: `podman init --latest diff --git a/cmd/podman/containers/inspect.go b/cmd/podman/containers/inspect.go index 03e6411a1..4195cf020 100644 --- a/cmd/podman/containers/inspect.go +++ b/cmd/podman/containers/inspect.go @@ -42,6 +42,6 @@ func init() { func inspectExec(cmd *cobra.Command, args []string) error { // Force container type - inspectOpts.Type = inspect.ContainerType + inspectOpts.Type = common.ContainerType return inspect.Inspect(args, *inspectOpts) } diff --git a/cmd/podman/containers/kill.go b/cmd/podman/containers/kill.go index 32f9899cd..eddefd196 100644 --- a/cmd/podman/containers/kill.go +++ b/cmd/podman/containers/kill.go @@ -25,7 +25,7 @@ var ( Long: killDescription, RunE: kill, Args: func(cmd *cobra.Command, args []string) error { - return validate.CheckAllLatestAndCIDFile(cmd, args, false, true) + return validate.CheckAllLatestAndIDFile(cmd, args, false, "cidfile") }, ValidArgsFunction: common.AutocompleteContainersRunning, Example: `podman kill mywebserver @@ -35,7 +35,7 @@ var ( containerKillCommand = &cobra.Command{ Args: func(cmd *cobra.Command, args []string) error { - return validate.CheckAllLatestAndCIDFile(cmd, args, false, true) + return validate.CheckAllLatestAndIDFile(cmd, args, false, "cidfile") }, Use: killCommand.Use, Short: killCommand.Short, @@ -95,7 +95,7 @@ func kill(_ *cobra.Command, args []string) error { return errors.New("valid signals are 1 through 64") } for _, cidFile := range cidFiles { - content, err := ioutil.ReadFile(string(cidFile)) + content, err := ioutil.ReadFile(cidFile) if err != nil { return errors.Wrap(err, "error reading CIDFile") } diff --git a/cmd/podman/containers/mount.go b/cmd/podman/containers/mount.go index 18177e3ce..16eb5d452 100644 --- a/cmd/podman/containers/mount.go +++ b/cmd/podman/containers/mount.go @@ -33,7 +33,7 @@ var ( Long: mountDescription, RunE: mount, Args: func(cmd *cobra.Command, args []string) error { - return validate.CheckAllLatestAndCIDFile(cmd, args, true, false) + return validate.CheckAllLatestAndIDFile(cmd, args, true, "") }, ValidArgsFunction: common.AutocompleteContainers, } diff --git a/cmd/podman/containers/port.go b/cmd/podman/containers/port.go index 22d1d16d3..f10bdd5b4 100644 --- a/cmd/podman/containers/port.go +++ b/cmd/podman/containers/port.go @@ -23,7 +23,7 @@ var ( Long: portDescription, RunE: port, Args: func(cmd *cobra.Command, args []string) error { - return validate.CheckAllLatestAndCIDFile(cmd, args, true, false) + return validate.CheckAllLatestAndIDFile(cmd, args, true, "") }, ValidArgsFunction: common.AutocompleteContainerOneArg, Example: `podman port --all @@ -37,7 +37,7 @@ var ( Long: portDescription, RunE: portCommand.RunE, Args: func(cmd *cobra.Command, args []string) error { - return validate.CheckAllLatestAndCIDFile(cmd, args, true, false) + return validate.CheckAllLatestAndIDFile(cmd, args, true, "") }, ValidArgsFunction: portCommand.ValidArgsFunction, Example: `podman container port --all diff --git a/cmd/podman/containers/restart.go b/cmd/podman/containers/restart.go index 69d8d71ea..25bbb61e3 100644 --- a/cmd/podman/containers/restart.go +++ b/cmd/podman/containers/restart.go @@ -26,7 +26,7 @@ var ( Long: restartDescription, RunE: restart, Args: func(cmd *cobra.Command, args []string) error { - return validate.CheckAllLatestAndCIDFile(cmd, args, false, false) + return validate.CheckAllLatestAndIDFile(cmd, args, false, "") }, ValidArgsFunction: common.AutocompleteContainers, Example: `podman restart ctrID diff --git a/cmd/podman/containers/restore.go b/cmd/podman/containers/restore.go index eeda5a05f..1e4745354 100644 --- a/cmd/podman/containers/restore.go +++ b/cmd/podman/containers/restore.go @@ -28,7 +28,7 @@ var ( Long: restoreDescription, RunE: restore, Args: func(cmd *cobra.Command, args []string) error { - return validate.CheckAllLatestAndCIDFile(cmd, args, true, false) + return validate.CheckAllLatestAndIDFile(cmd, args, true, "") }, ValidArgsFunction: common.AutocompleteContainersAndImages, Example: `podman container restore ctrID diff --git a/cmd/podman/containers/rm.go b/cmd/podman/containers/rm.go index 7e0955863..bcbe86947 100644 --- a/cmd/podman/containers/rm.go +++ b/cmd/podman/containers/rm.go @@ -28,7 +28,7 @@ var ( Long: rmDescription, RunE: rm, Args: func(cmd *cobra.Command, args []string) error { - return validate.CheckAllLatestAndCIDFile(cmd, args, false, true) + return validate.CheckAllLatestAndIDFile(cmd, args, false, "cidfile") }, ValidArgsFunction: common.AutocompleteContainers, Example: `podman rm imageID @@ -102,7 +102,7 @@ func rm(cmd *cobra.Command, args []string) error { rmOptions.Timeout = &stopTimeout } for _, cidFile := range cidFiles { - content, err := ioutil.ReadFile(string(cidFile)) + content, err := ioutil.ReadFile(cidFile) if err != nil { return errors.Wrap(err, "error reading CIDFile") } @@ -123,9 +123,7 @@ func rm(cmd *cobra.Command, args []string) error { // removeContainers will set the exit code according to the `podman-rm` man // page. func removeContainers(namesOrIDs []string, rmOptions entities.RmOptions, setExit bool) error { - var ( - errs utils.OutputErrors - ) + var errs utils.OutputErrors responses, err := registry.ContainerEngine().ContainerRm(context.Background(), namesOrIDs, rmOptions) if err != nil { if setExit { @@ -135,8 +133,9 @@ func removeContainers(namesOrIDs []string, rmOptions entities.RmOptions, setExit } for _, r := range responses { if r.Err != nil { - // TODO this will not work with the remote client - if errors.Cause(err) == define.ErrWillDeadlock { + // When using the API, errors.Cause(err) will never equal constant define.ErrWillDeadLock + if errors.Cause(r.Err) == define.ErrWillDeadlock || + errors.Cause(r.Err).Error() == define.ErrWillDeadlock.Error() { logrus.Errorf("Potential deadlock detected - please run 'podman system renumber' to resolve") } if setExit { diff --git a/cmd/podman/containers/run.go b/cmd/podman/containers/run.go index 951981293..a6c500afa 100644 --- a/cmd/podman/containers/run.go +++ b/cmd/podman/containers/run.go @@ -109,7 +109,9 @@ func init() { } func run(cmd *cobra.Command, args []string) error { - var err error + if err := commonFlags(cmd); err != nil { + return err + } // TODO: Breaking change should be made fatal in next major Release if cliVals.TTY && cliVals.Interactive && !term.IsTerminal(int(os.Stdin.Fd())) { @@ -122,14 +124,10 @@ func run(cmd *cobra.Command, args []string) error { } } - flags := cmd.Flags() - cliVals.Net, err = common.NetFlagsToNetOptions(nil, *flags) - if err != nil { - return err - } runOpts.CIDFile = cliVals.CIDFile runOpts.Rm = cliVals.Rm - if cliVals, err = CreateInit(cmd, cliVals, false); err != nil { + cliVals, err := CreateInit(cmd, cliVals, false) + if err != nil { return err } diff --git a/cmd/podman/containers/stats.go b/cmd/podman/containers/stats.go index 500671d31..02f8c6970 100644 --- a/cmd/podman/containers/stats.go +++ b/cmd/podman/containers/stats.go @@ -214,10 +214,6 @@ func (s *containerStats) BlockIO() string { } func (s *containerStats) PIDS() string { - if s.PIDs == 0 { - // If things go bazinga, return a safe value - return "--" - } return fmt.Sprintf("%d", s.PIDs) } @@ -231,7 +227,7 @@ func (s *containerStats) MemUsageBytes() string { func floatToPercentString(f float64) string { strippedFloat, err := utils.RemoveScientificNotationFromFloat(f) - if err != nil || strippedFloat == 0 { + if err != nil { // If things go bazinga, return a safe value return "--" } @@ -239,16 +235,10 @@ func floatToPercentString(f float64) string { } func combineHumanValues(a, b uint64) string { - if a == 0 && b == 0 { - return "-- / --" - } return fmt.Sprintf("%s / %s", units.HumanSize(float64(a)), units.HumanSize(float64(b))) } func combineBytesValues(a, b uint64) string { - if a == 0 && b == 0 { - return "-- / --" - } return fmt.Sprintf("%s / %s", units.BytesSize(float64(a)), units.BytesSize(float64(b))) } diff --git a/cmd/podman/containers/stop.go b/cmd/podman/containers/stop.go index 381997fee..def608fea 100644 --- a/cmd/podman/containers/stop.go +++ b/cmd/podman/containers/stop.go @@ -26,7 +26,7 @@ var ( Long: stopDescription, RunE: stop, Args: func(cmd *cobra.Command, args []string) error { - return validate.CheckAllLatestAndCIDFile(cmd, args, false, true) + return validate.CheckAllLatestAndIDFile(cmd, args, false, "cidfile") }, ValidArgsFunction: common.AutocompleteContainersRunning, Example: `podman stop ctrID @@ -40,7 +40,7 @@ var ( Long: stopCommand.Long, RunE: stopCommand.RunE, Args: func(cmd *cobra.Command, args []string) error { - return validate.CheckAllLatestAndCIDFile(cmd, args, false, true) + return validate.CheckAllLatestAndIDFile(cmd, args, false, "cidfile") }, ValidArgsFunction: stopCommand.ValidArgsFunction, Example: `podman container stop ctrID @@ -100,7 +100,7 @@ func stop(cmd *cobra.Command, args []string) error { } for _, cidFile := range cidFiles { - content, err := ioutil.ReadFile(string(cidFile)) + content, err := ioutil.ReadFile(cidFile) if err != nil { return errors.Wrap(err, "error reading CIDFile") } diff --git a/cmd/podman/containers/unmount.go b/cmd/podman/containers/unmount.go index 26b8cfcc5..6869de2e2 100644 --- a/cmd/podman/containers/unmount.go +++ b/cmd/podman/containers/unmount.go @@ -27,7 +27,7 @@ var ( Long: description, RunE: unmount, Args: func(cmd *cobra.Command, args []string) error { - return validate.CheckAllLatestAndCIDFile(cmd, args, false, false) + return validate.CheckAllLatestAndIDFile(cmd, args, false, "") }, ValidArgsFunction: common.AutocompleteContainers, Example: `podman unmount ctrID @@ -43,7 +43,7 @@ var ( Long: unmountCommand.Long, RunE: unmountCommand.RunE, Args: func(cmd *cobra.Command, args []string) error { - return validate.CheckAllLatestAndCIDFile(cmd, args, false, false) + return validate.CheckAllLatestAndIDFile(cmd, args, false, "") }, ValidArgsFunction: common.AutocompleteContainers, Example: `podman container unmount ctrID diff --git a/cmd/podman/diff.go b/cmd/podman/diff.go index 7b78c8312..ec98fb5b5 100644 --- a/cmd/podman/diff.go +++ b/cmd/podman/diff.go @@ -35,9 +35,6 @@ func init() { Command: diffCmd, }) flags := diffCmd.Flags() - // FIXME: Why does this exists? It is not used anywhere. - flags.BoolVar(&diffOpts.Archive, "archive", true, "Save the diff as a tar archive") - _ = flags.MarkHidden("archive") formatFlagName := "format" flags.StringVar(&diffOpts.Format, formatFlagName, "", "Change the output format (json)") diff --git a/cmd/podman/diff/diff.go b/cmd/podman/diff/diff.go index a26502de9..15c55852a 100644 --- a/cmd/podman/diff/diff.go +++ b/cmd/podman/diff/diff.go @@ -13,7 +13,7 @@ import ( "github.com/spf13/cobra" ) -func Diff(cmd *cobra.Command, args []string, options entities.DiffOptions) error { +func Diff(_ *cobra.Command, args []string, options entities.DiffOptions) error { results, err := registry.ContainerEngine().Diff(registry.GetContext(), args, options) if err != nil { return err @@ -63,7 +63,7 @@ func changesToTable(diffs *entities.DiffReport) error { return nil } -// IDOrLatestArgs used to validate a nameOrId was provided or the "--latest" flag +// ValidateContainerDiffArgs used to validate a nameOrId was provided or the "--latest" flag func ValidateContainerDiffArgs(cmd *cobra.Command, args []string) error { given, _ := cmd.Flags().GetBool("latest") if len(args) > 0 && !given { diff --git a/cmd/podman/images/build.go b/cmd/podman/images/build.go index 3ea60e18a..94b7c43a2 100644 --- a/cmd/podman/images/build.go +++ b/cmd/podman/images/build.go @@ -191,15 +191,15 @@ func buildFlags(cmd *cobra.Command) { _ = flags.MarkHidden("compress") _ = flags.MarkHidden("volume") _ = flags.MarkHidden("output") + _ = flags.MarkHidden("logsplit") } } // build executes the build command. func build(cmd *cobra.Command, args []string) error { if (cmd.Flags().Changed("squash") && cmd.Flags().Changed("layers")) || - (cmd.Flags().Changed("squash-all") && cmd.Flags().Changed("layers")) || (cmd.Flags().Changed("squash-all") && cmd.Flags().Changed("squash")) { - return errors.New("cannot specify --squash, --squash-all and --layers options together") + return errors.New("cannot specify --squash with --layers and --squash-all with --squash") } if cmd.Flag("output").Changed && registry.IsRemote() { @@ -418,7 +418,13 @@ func buildFlagsWrapperToOptions(c *cobra.Command, contextDir string, flags *buil // Squash-all invoked, squash both new and old layers into one. if c.Flags().Changed("squash-all") { flags.Squash = true - flags.Layers = false + if !c.Flags().Changed("layers") { + // Buildah supports using layers and --squash together + // after https://github.com/containers/buildah/pull/3674 + // so podman must honor if user wants to still use layers + // with --squash-all. + flags.Layers = false + } } var stdin io.Reader @@ -442,22 +448,6 @@ func buildFlagsWrapperToOptions(c *cobra.Command, contextDir string, flags *buil return nil, err } - // `buildah bud --layers=false` acts like `docker build --squash` does. - // That is all of the new layers created during the build process are - // condensed into one, any layers present prior to this build are retained - // without condensing. `buildah bud --squash` squashes both new and old - // layers down into one. Translate Podman commands into Buildah. - // Squash invoked, retain old layers, squash new layers into one. - if c.Flags().Changed("squash") && flags.Squash { - flags.Squash = false - flags.Layers = false - } - // Squash-all invoked, squash both new and old layers into one. - if c.Flags().Changed("squash-all") { - flags.Squash = true - flags.Layers = false - } - compression := buildahDefine.Gzip if flags.DisableCompression { compression = buildahDefine.Uncompressed @@ -513,9 +503,26 @@ func buildFlagsWrapperToOptions(c *cobra.Command, contextDir string, flags *buil return nil, errors.Wrapf(err, "unable to obtain decrypt config") } + additionalBuildContext := make(map[string]*buildahDefine.AdditionalBuildContext) + if c.Flag("build-context").Changed { + for _, contextString := range flags.BuildContext { + av := strings.SplitN(contextString, "=", 2) + if len(av) > 1 { + parseAdditionalBuildContext, err := parse.GetAdditionalBuildContext(av[1]) + if err != nil { + return nil, errors.Wrapf(err, "while parsing additional build context") + } + additionalBuildContext[av[0]] = &parseAdditionalBuildContext + } else { + return nil, fmt.Errorf("while parsing additional build context: %q, accepts value in the form of key=value", av) + } + } + } + opts := buildahDefine.BuildOptions{ AddCapabilities: flags.CapAdd, AdditionalTags: tags, + AdditionalBuildContexts: additionalBuildContext, AllPlatforms: flags.AllPlatforms, Annotations: flags.Annotation, Args: args, @@ -525,6 +532,7 @@ func buildFlagsWrapperToOptions(c *cobra.Command, contextDir string, flags *buil Compression: compression, ConfigureNetwork: networkPolicy, ContextDirectory: contextDir, + CPPFlags: flags.CPPFlags, DefaultMountsFilePath: containerConfig.Containers.DefaultMountsFile, Devices: flags.Devices, DropCapabilities: flags.CapDrop, @@ -539,6 +547,8 @@ func buildFlagsWrapperToOptions(c *cobra.Command, contextDir string, flags *buil Labels: flags.Label, Layers: flags.Layers, LogRusage: flags.LogRusage, + LogFile: flags.Logfile, + LogSplitByPlatform: flags.LogSplitByPlatform, Manifest: flags.Manifest, MaxPullPushRetries: 3, NamespaceOptions: nsValues, diff --git a/cmd/podman/images/diff.go b/cmd/podman/images/diff.go index 13a8f1d9d..a017d569d 100644 --- a/cmd/podman/images/diff.go +++ b/cmd/podman/images/diff.go @@ -34,9 +34,7 @@ func init() { } func diffFlags(flags *pflag.FlagSet) { - diffOpts = &entities.DiffOptions{} - flags.BoolVar(&diffOpts.Archive, "archive", true, "Save the diff as a tar archive") - _ = flags.MarkDeprecated("archive", "Provided for backwards compatibility, has no impact on output.") + diffOpts = new(entities.DiffOptions) formatFlagName := "format" flags.StringVar(&diffOpts.Format, formatFlagName, "", "Change the output format (json)") diff --git a/cmd/podman/images/inspect.go b/cmd/podman/images/inspect.go index 22c404b3f..310f8cda8 100644 --- a/cmd/podman/images/inspect.go +++ b/cmd/podman/images/inspect.go @@ -38,6 +38,6 @@ func init() { } func inspectExec(cmd *cobra.Command, args []string) error { - inspectOpts.Type = inspect.ImageType + inspectOpts.Type = common.ImageType return inspect.Inspect(args, *inspectOpts) } diff --git a/cmd/podman/images/load.go b/cmd/podman/images/load.go index dbb7c32fa..c18c32387 100644 --- a/cmd/podman/images/load.go +++ b/cmd/podman/images/load.go @@ -110,6 +110,6 @@ func load(cmd *cobra.Command, args []string) error { if err != nil { return err } - fmt.Println("Loaded image(s): " + strings.Join(response.Names, ",")) + fmt.Println("Loaded image: " + strings.Join(response.Names, "\nLoaded image: ")) return nil } diff --git a/cmd/podman/images/mount.go b/cmd/podman/images/mount.go index d5ab3d274..532d96196 100644 --- a/cmd/podman/images/mount.go +++ b/cmd/podman/images/mount.go @@ -7,7 +7,6 @@ import ( "github.com/containers/common/pkg/report" "github.com/containers/podman/v4/cmd/podman/common" "github.com/containers/podman/v4/cmd/podman/registry" - "github.com/containers/podman/v4/cmd/podman/utils" "github.com/containers/podman/v4/pkg/domain/entities" "github.com/pkg/errors" "github.com/spf13/cobra" @@ -71,16 +70,12 @@ func mount(cmd *cobra.Command, args []string) error { return err } - if len(args) > 0 || mountOpts.All { - var errs utils.OutputErrors - for _, r := range reports { - if r.Err == nil { - fmt.Println(r.Path) - continue - } - errs = append(errs, r.Err) + if len(args) == 1 && mountOpts.Format == "" && !mountOpts.All { + if len(reports) != 1 { + return fmt.Errorf("internal error: expected 1 report but got %d", len(reports)) } - return errs.PrintErrors() + fmt.Println(reports[0].Path) + return nil } switch { diff --git a/cmd/podman/images/push.go b/cmd/podman/images/push.go index a59bdd93c..1b3419014 100644 --- a/cmd/podman/images/push.go +++ b/cmd/podman/images/push.go @@ -117,7 +117,6 @@ func pushFlags(cmd *cobra.Command) { _ = flags.MarkHidden("compress") _ = flags.MarkHidden("digestfile") _ = flags.MarkHidden("quiet") - _ = flags.MarkHidden("remove-signatures") _ = flags.MarkHidden("sign-by") } if !registry.IsRemote() { diff --git a/cmd/podman/images/save.go b/cmd/podman/images/save.go index fb642bafd..d85d688ee 100644 --- a/cmd/podman/images/save.go +++ b/cmd/podman/images/save.go @@ -6,19 +6,18 @@ import ( "strings" "github.com/containers/common/pkg/completion" + "github.com/containers/common/pkg/util" "github.com/containers/podman/v4/cmd/podman/common" "github.com/containers/podman/v4/cmd/podman/parse" "github.com/containers/podman/v4/cmd/podman/registry" "github.com/containers/podman/v4/libpod/define" "github.com/containers/podman/v4/pkg/domain/entities" - "github.com/containers/podman/v4/pkg/util" "github.com/pkg/errors" "github.com/spf13/cobra" "golang.org/x/term" ) var ( - validFormats = []string{define.OCIManifestDir, define.OCIArchive, define.V2s2ManifestDir, define.V2s2Archive} containerConfig = registry.PodmanConfig() ) @@ -38,8 +37,8 @@ var ( if err != nil { return err } - if !util.StringInSlice(format, validFormats) { - return errors.Errorf("format value must be one of %s", strings.Join(validFormats, " ")) + if !util.StringInSlice(format, common.ValidSaveFormats) { + return errors.Errorf("format value must be one of %s", strings.Join(common.ValidSaveFormats, " ")) } return nil }, diff --git a/cmd/podman/images/search.go b/cmd/podman/images/search.go index 335ea2b5a..a18f7a11d 100644 --- a/cmd/podman/images/search.go +++ b/cmd/podman/images/search.go @@ -83,8 +83,7 @@ func searchFlags(cmd *cobra.Command) { filterFlagName := "filter" flags.StringSliceVarP(&searchOptions.Filters, filterFlagName, "f", []string{}, "Filter output based on conditions provided (default [])") - // TODO add custom filter function - _ = cmd.RegisterFlagCompletionFunc(filterFlagName, completion.AutocompleteNone) + _ = cmd.RegisterFlagCompletionFunc(filterFlagName, common.AutocompleteImageSearchFilters) formatFlagName := "format" flags.StringVar(&searchOptions.Format, formatFlagName, "", "Change the output format to JSON or a Go template") diff --git a/cmd/podman/images/trust_set.go b/cmd/podman/images/trust_set.go index fff035d12..f4ff0cffc 100644 --- a/cmd/podman/images/trust_set.go +++ b/cmd/podman/images/trust_set.go @@ -5,10 +5,10 @@ import ( "regexp" "github.com/containers/common/pkg/completion" + "github.com/containers/common/pkg/util" "github.com/containers/podman/v4/cmd/podman/common" "github.com/containers/podman/v4/cmd/podman/registry" "github.com/containers/podman/v4/pkg/domain/entities" - "github.com/containers/podman/v4/pkg/util" "github.com/pkg/errors" "github.com/spf13/cobra" ) diff --git a/cmd/podman/inspect/inspect.go b/cmd/podman/inspect/inspect.go index b26b2d667..f6e3fca06 100644 --- a/cmd/podman/inspect/inspect.go +++ b/cmd/podman/inspect/inspect.go @@ -21,21 +21,6 @@ import ( "github.com/spf13/cobra" ) -const ( - // AllType can be of type ImageType or ContainerType. - AllType = "all" - // ContainerType is the container type. - ContainerType = "container" - // ImageType is the image type. - ImageType = "image" - // NetworkType is the network type - NetworkType = "network" - // PodType is the pod type. - PodType = "pod" - // VolumeType is the volume type - VolumeType = "volume" -) - // AddInspectFlagSet takes a command and adds the inspect flags and returns an // InspectOptions object. func AddInspectFlagSet(cmd *cobra.Command) *entities.InspectOptions { @@ -49,7 +34,7 @@ func AddInspectFlagSet(cmd *cobra.Command) *entities.InspectOptions { _ = cmd.RegisterFlagCompletionFunc(formatFlagName, completion.AutocompleteNone) typeFlagName := "type" - flags.StringVarP(&opts.Type, typeFlagName, "t", AllType, fmt.Sprintf("Specify inspect-object type (%q, %q or %q)", ImageType, ContainerType, AllType)) + flags.StringVarP(&opts.Type, typeFlagName, "t", common.AllType, "Specify inspect-object type") _ = cmd.RegisterFlagCompletionFunc(typeFlagName, common.AutocompleteInspectType) validate.AddLatestFlag(cmd, &opts.Latest) @@ -76,21 +61,22 @@ type inspector struct { // newInspector creates a new inspector based on the specified options. func newInspector(options entities.InspectOptions) (*inspector, error) { switch options.Type { - case ImageType, ContainerType, AllType, PodType, NetworkType, VolumeType: + case common.ImageType, common.ContainerType, common.AllType, common.PodType, common.NetworkType, common.VolumeType: // Valid types. default: - return nil, errors.Errorf("invalid type %q: must be %q, %q, %q, %q, %q, or %q", options.Type, ImageType, ContainerType, PodType, NetworkType, VolumeType, AllType) + return nil, errors.Errorf("invalid type %q: must be %q, %q, %q, %q, %q, or %q", options.Type, + common.ImageType, common.ContainerType, common.PodType, common.NetworkType, common.VolumeType, common.AllType) } - if options.Type == ImageType { + if options.Type == common.ImageType { if options.Latest { - return nil, errors.Errorf("latest is not supported for type %q", ImageType) + return nil, errors.Errorf("latest is not supported for type %q", common.ImageType) } if options.Size { - return nil, errors.Errorf("size is not supported for type %q", ImageType) + return nil, errors.Errorf("size is not supported for type %q", common.ImageType) } } - if options.Type == PodType && options.Size { - return nil, errors.Errorf("size is not supported for type %q", PodType) + if options.Type == common.PodType && options.Size { + return nil, errors.Errorf("size is not supported for type %q", common.PodType) } podOpts := entities.PodInspectOptions{ Latest: options.Latest, @@ -122,21 +108,21 @@ func (i *inspector) inspect(namesOrIDs []string) error { if len(namesOrIDs) > 0 { return errors.New("--latest and arguments cannot be used together") } - if i.options.Type == AllType { - tmpType = ContainerType // -l works with --type=all, defaults to containertype + if i.options.Type == common.AllType { + tmpType = common.ContainerType // -l works with --type=all, defaults to containertype } } // Inspect - note that AllType requires us to expensively query one-by-one. switch tmpType { - case AllType: + case common.AllType: allData, allErrs, err := i.inspectAll(ctx, namesOrIDs) if err != nil { return err } data = allData errs = allErrs - case ImageType: + case common.ImageType: imgData, allErrs, err := i.imageEngine.Inspect(ctx, namesOrIDs, i.options) if err != nil { return err @@ -145,7 +131,7 @@ func (i *inspector) inspect(namesOrIDs []string) error { for i := range imgData { data = append(data, imgData[i]) } - case ContainerType: + case common.ContainerType: ctrData, allErrs, err := i.containerEngine.ContainerInspect(ctx, namesOrIDs, i.options) if err != nil { return err @@ -154,7 +140,7 @@ func (i *inspector) inspect(namesOrIDs []string) error { for i := range ctrData { data = append(data, ctrData[i]) } - case PodType: + case common.PodType: for _, pod := range namesOrIDs { i.podOptions.NameOrID = pod podData, err := i.containerEngine.PodInspect(ctx, i.podOptions) @@ -184,7 +170,7 @@ func (i *inspector) inspect(namesOrIDs []string) error { data = append(data, podData) } } - case NetworkType: + case common.NetworkType: networkData, allErrs, err := registry.ContainerEngine().NetworkInspect(ctx, namesOrIDs, i.options) if err != nil { return err @@ -193,7 +179,7 @@ func (i *inspector) inspect(namesOrIDs []string) error { for i := range networkData { data = append(data, networkData[i]) } - case VolumeType: + case common.VolumeType: volumeData, allErrs, err := i.containerEngine.VolumeInspect(ctx, namesOrIDs, i.options) if err != nil { return err @@ -203,7 +189,8 @@ func (i *inspector) inspect(namesOrIDs []string) error { data = append(data, volumeData[i]) } default: - return errors.Errorf("invalid type %q: must be %q, %q, %q, %q, %q, or %q", i.options.Type, ImageType, ContainerType, PodType, NetworkType, VolumeType, AllType) + return errors.Errorf("invalid type %q: must be %q, %q, %q, %q, %q, or %q", i.options.Type, + common.ImageType, common.ContainerType, common.PodType, common.NetworkType, common.VolumeType, common.AllType) } // Always print an empty array if data == nil { diff --git a/cmd/podman/machine/init.go b/cmd/podman/machine/init.go index 6c31f3531..612c36057 100644 --- a/cmd/podman/machine/init.go +++ b/cmd/podman/machine/init.go @@ -25,16 +25,14 @@ var ( Example: `podman machine init myvm`, ValidArgsFunction: completion.AutocompleteNone, } -) -var ( initOpts = machine.InitOptions{} defaultMachineName = machine.DefaultMachineName now bool ) // maxMachineNameSize is set to thirty to limit huge machine names primarily -// because macos has a much smaller file size limit. +// because macOS has a much smaller file size limit. const maxMachineNameSize = 30 func init() { @@ -111,8 +109,7 @@ func init() { flags.BoolVar(&initOpts.Rootful, rootfulFlagName, false, "Whether this machine should prefer rootful container execution") } -// TODO should we allow for a users to append to the qemu cmdline? -func initMachine(cmd *cobra.Command, args []string) error { +func initMachine(_ *cobra.Command, args []string) error { var ( err error vm machine.VM @@ -122,7 +119,7 @@ func initMachine(cmd *cobra.Command, args []string) error { initOpts.Name = defaultMachineName if len(args) > 0 { if len(args[0]) > maxMachineNameSize { - return errors.New("machine name must be 30 characters or less") + return errors.Errorf("machine name %q must be %d characters or less", args[0], maxMachineNameSize) } initOpts.Name = args[0] } diff --git a/cmd/podman/machine/list.go b/cmd/podman/machine/list.go index 5254d50cf..bb14d4a67 100644 --- a/cmd/podman/machine/list.go +++ b/cmd/podman/machine/list.go @@ -48,6 +48,7 @@ type ListReporter struct { Default bool Created string Running bool + Starting bool LastUp string Stream string VMType string @@ -224,10 +225,14 @@ func toHumanFormat(vms []*machine.ListResponse) ([]*ListReporter, error) { } else { response.Name = vm.Name } - if vm.Running { + switch { + case vm.Running: response.LastUp = "Currently running" response.Running = true - } else { + case vm.Starting: + response.LastUp = "Currently starting" + response.Starting = true + default: response.LastUp = units.HumanDuration(time.Since(vm.LastUp)) + " ago" } response.Created = units.HumanDuration(time.Since(vm.CreatedAt)) + " ago" diff --git a/cmd/podman/machine/ssh.go b/cmd/podman/machine/ssh.go index 4a86da67a..8261f3607 100644 --- a/cmd/podman/machine/ssh.go +++ b/cmd/podman/machine/ssh.go @@ -9,6 +9,7 @@ import ( "github.com/containers/common/pkg/completion" "github.com/containers/common/pkg/config" "github.com/containers/podman/v4/cmd/podman/registry" + "github.com/containers/podman/v4/cmd/podman/utils" "github.com/containers/podman/v4/pkg/machine" "github.com/pkg/errors" "github.com/spf13/cobra" @@ -89,7 +90,8 @@ func ssh(cmd *cobra.Command, args []string) error { if err != nil { return errors.Wrapf(err, "vm %s not found", vmName) } - return vm.SSH(vmName, sshOpts) + err = vm.SSH(vmName, sshOpts) + return utils.HandleOSExecError(err) } func remoteConnectionUsername() (string, error) { diff --git a/cmd/podman/machine/start.go b/cmd/podman/machine/start.go index c9b99e63b..3bd7f4a25 100644 --- a/cmd/podman/machine/start.go +++ b/cmd/podman/machine/start.go @@ -56,7 +56,7 @@ func start(_ *cobra.Command, args []string) error { if vmName == activeName { return errors.Wrapf(machine.ErrVMAlreadyRunning, "cannot start VM %s", vmName) } - return errors.Wrapf(machine.ErrMultipleActiveVM, "cannot start VM %s. VM %s is currently running", vmName, activeName) + return errors.Wrapf(machine.ErrMultipleActiveVM, "cannot start VM %s. VM %s is currently running or starting", vmName, activeName) } fmt.Printf("Starting machine %q\n", vmName) if err := vm.Start(vmName, machine.StartOptions{}); err != nil { diff --git a/cmd/podman/networks/inspect.go b/cmd/podman/networks/inspect.go index 8f39ec395..1a8444147 100644 --- a/cmd/podman/networks/inspect.go +++ b/cmd/podman/networks/inspect.go @@ -37,6 +37,6 @@ func init() { } func networkInspect(_ *cobra.Command, args []string) error { - inspectOpts.Type = inspect.NetworkType + inspectOpts.Type = common.NetworkType return inspect.Inspect(args, *inspectOpts) } diff --git a/cmd/podman/networks/reload.go b/cmd/podman/networks/reload.go index 7b6323187..66248e9fb 100644 --- a/cmd/podman/networks/reload.go +++ b/cmd/podman/networks/reload.go @@ -21,7 +21,7 @@ var ( Long: networkReloadDescription, RunE: networkReload, Args: func(cmd *cobra.Command, args []string) error { - return validate.CheckAllLatestAndCIDFile(cmd, args, false, false) + return validate.CheckAllLatestAndIDFile(cmd, args, false, "") }, ValidArgsFunction: common.AutocompleteContainers, Example: `podman network reload --latest diff --git a/cmd/podman/parse/net.go b/cmd/podman/parse/net.go index 870690db3..b616e1029 100644 --- a/cmd/podman/parse/net.go +++ b/cmd/podman/parse/net.go @@ -18,6 +18,8 @@ import ( const ( Protocol_TCP Protocol = 0 Protocol_UDP Protocol = 1 + LabelType string = "label" + ENVType string = "env" ) type Protocol int32 @@ -89,9 +91,7 @@ func GetAllLabels(labelFile, inputLabels []string) (map[string]string, error) { // There's an argument that we SHOULD be doing that parsing for // all environment variables, even those sourced from files, but // that would require a substantial rework. - if err := parseEnvFile(labels, file); err != nil { - // FIXME: parseEnvFile is using parseEnv, so we need to add extra - // logic for labels. + if err := parseEnvOrLabelFile(labels, file, LabelType); err != nil { return nil, err } } @@ -109,7 +109,7 @@ func GetAllLabels(labelFile, inputLabels []string) (map[string]string, error) { return labels, nil } -func parseEnv(env map[string]string, line string) error { +func parseEnvOrLabel(env map[string]string, line, configType string) error { data := strings.SplitN(line, "=", 2) // catch invalid variables such as "=" or "=A" @@ -137,7 +137,7 @@ func parseEnv(env map[string]string, line string) error { env[part[0]] = part[1] } } - } else { + } else if configType == ENVType { // if only a pass-through variable is given, clean it up. if val, ok := os.LookupEnv(name); ok { env[name] = val @@ -147,8 +147,9 @@ func parseEnv(env map[string]string, line string) error { return nil } -// parseEnvFile reads a file with environment variables enumerated by lines -func parseEnvFile(env map[string]string, filename string) error { +// parseEnvOrLabelFile reads a file with environment variables enumerated by lines +// configType should be set to either "label" or "env" based on what type is being parsed +func parseEnvOrLabelFile(envOrLabel map[string]string, filename, configType string) error { fh, err := os.Open(filename) if err != nil { return err @@ -161,7 +162,7 @@ func parseEnvFile(env map[string]string, filename string) error { line := strings.TrimLeft(scanner.Text(), whiteSpaces) // line is not empty, and not starting with '#' if len(line) > 0 && !strings.HasPrefix(line, "#") { - if err := parseEnv(env, line); err != nil { + if err := parseEnvOrLabel(envOrLabel, line, configType); err != nil { return err } } diff --git a/cmd/podman/pods/create.go b/cmd/podman/pods/create.go index 62f820790..e2f80bdbc 100644 --- a/cmd/podman/pods/create.go +++ b/cmd/podman/pods/create.go @@ -16,7 +16,6 @@ import ( "github.com/containers/podman/v4/cmd/podman/containers" "github.com/containers/podman/v4/cmd/podman/parse" "github.com/containers/podman/v4/cmd/podman/registry" - "github.com/containers/podman/v4/cmd/podman/validate" "github.com/containers/podman/v4/libpod/define" "github.com/containers/podman/v4/pkg/domain/entities" "github.com/containers/podman/v4/pkg/errorhandling" @@ -36,12 +35,14 @@ var ( You can then start it at any time with the podman pod start <pod_id> command. The pod will be created with the initial state 'created'.` createCommand = &cobra.Command{ - Use: "create [options]", - Args: validate.NoArgs, + Use: "create [options] [NAME]", + Args: cobra.MaximumNArgs(1), Short: "Create a new empty pod", Long: podCreateDescription, RunE: create, ValidArgsFunction: completion.AutocompleteNone, + Example: `podman pod create + podman pod create --label foo=bar mypod`, } ) @@ -115,6 +116,12 @@ func create(cmd *cobra.Command, args []string) error { rawImageName string podName string ) + if len(args) > 0 { + if len(createOptions.Name) > 0 { + return fmt.Errorf("cannot specify --name and NAME at the same time") + } + createOptions.Name = args[0] + } labelFile = infraOptions.LabelFile labels = infraOptions.Label createOptions.Labels, err = parse.GetAllLabels(labelFile, labels) @@ -128,7 +135,7 @@ func create(cmd *cobra.Command, args []string) error { img := imageName if !createOptions.Infra { if cmd.Flag("no-hosts").Changed { - return fmt.Errorf("cannot specify no-hosts without an infra container") + return fmt.Errorf("cannot specify --no-hosts without an infra container") } flags := cmd.Flags() createOptions.Net, err = common.NetFlagsToNetOptions(nil, *flags) @@ -159,7 +166,12 @@ func create(cmd *cobra.Command, args []string) error { if strings.Contains(share, "cgroup") && shareParent { return errors.Wrapf(define.ErrInvalidArg, "cannot define the pod as the cgroup parent at the same time as joining the infra container's cgroupNS") } - createOptions.Share = strings.Split(share, ",") + + if strings.HasPrefix(share, "+") { + createOptions.Share = append(createOptions.Share, strings.Split(specgen.DefaultKernelNamespaces, ",")...) + share = share[1:] + } + createOptions.Share = append(createOptions.Share, strings.Split(share, ",")...) createOptions.ShareParent = &shareParent if cmd.Flag("infra-command").Changed { // Only send content to server side if user changed defaults diff --git a/cmd/podman/pods/kill.go b/cmd/podman/pods/kill.go index 7216e08bb..5d3b15dc3 100644 --- a/cmd/podman/pods/kill.go +++ b/cmd/podman/pods/kill.go @@ -22,7 +22,7 @@ var ( Long: podKillDescription, RunE: kill, Args: func(cmd *cobra.Command, args []string) error { - return validate.CheckAllLatestAndCIDFile(cmd, args, false, false) + return validate.CheckAllLatestAndIDFile(cmd, args, false, "") }, ValidArgsFunction: common.AutocompletePodsRunning, Example: `podman pod kill podID diff --git a/cmd/podman/pods/pause.go b/cmd/podman/pods/pause.go index adc54d171..389fb8415 100644 --- a/cmd/podman/pods/pause.go +++ b/cmd/podman/pods/pause.go @@ -22,7 +22,7 @@ var ( Long: podPauseDescription, RunE: pause, Args: func(cmd *cobra.Command, args []string) error { - return validate.CheckAllLatestAndCIDFile(cmd, args, false, false) + return validate.CheckAllLatestAndIDFile(cmd, args, false, "") }, ValidArgsFunction: common.AutocompletePodsRunning, Example: `podman pod pause podID1 podID2 diff --git a/cmd/podman/pods/ps.go b/cmd/podman/pods/ps.go index 1275e65dc..aa42e1983 100644 --- a/cmd/podman/pods/ps.go +++ b/cmd/podman/pods/ps.go @@ -24,7 +24,7 @@ var ( // Command: podman pod _ps_ psCmd = &cobra.Command{ - Use: "ps [options]", + Use: "ps [options]", Aliases: []string{"ls", "list"}, Short: "List pods", Long: psDescription, @@ -49,7 +49,6 @@ func init() { flags.BoolVar(&psInput.CtrNames, "ctr-names", false, "Display the container names") flags.BoolVar(&psInput.CtrIds, "ctr-ids", false, "Display the container UUIDs. If no-trunc is not set they will be truncated") flags.BoolVar(&psInput.CtrStatus, "ctr-status", false, "Display the container status") - // TODO should we make this a [] ? filterFlagName := "filter" flags.StringSliceVarP(&inputFilters, filterFlagName, "f", []string{}, "Filter output based on conditions given") diff --git a/cmd/podman/pods/restart.go b/cmd/podman/pods/restart.go index 6d624806a..a8e31ce07 100644 --- a/cmd/podman/pods/restart.go +++ b/cmd/podman/pods/restart.go @@ -22,7 +22,7 @@ var ( Long: podRestartDescription, RunE: restart, Args: func(cmd *cobra.Command, args []string) error { - return validate.CheckAllLatestAndCIDFile(cmd, args, false, false) + return validate.CheckAllLatestAndIDFile(cmd, args, false, "") }, ValidArgsFunction: common.AutocompletePods, Example: `podman pod restart podID1 podID2 diff --git a/cmd/podman/pods/rm.go b/cmd/podman/pods/rm.go index 52a815534..16b7191c9 100644 --- a/cmd/podman/pods/rm.go +++ b/cmd/podman/pods/rm.go @@ -35,7 +35,7 @@ var ( Long: podRmDescription, RunE: rm, Args: func(cmd *cobra.Command, args []string) error { - return validate.CheckAllLatestAndPodIDFile(cmd, args, false, true) + return validate.CheckAllLatestAndIDFile(cmd, args, false, "pod-id-file") }, ValidArgsFunction: common.AutocompletePods, Example: `podman pod rm mywebserverpod diff --git a/cmd/podman/pods/start.go b/cmd/podman/pods/start.go index b668cdd61..9436d34a5 100644 --- a/cmd/podman/pods/start.go +++ b/cmd/podman/pods/start.go @@ -31,7 +31,7 @@ var ( Long: podStartDescription, RunE: start, Args: func(cmd *cobra.Command, args []string) error { - return validate.CheckAllLatestAndPodIDFile(cmd, args, false, true) + return validate.CheckAllLatestAndIDFile(cmd, args, false, "pod-id-file") }, ValidArgsFunction: common.AutocompletePods, Example: `podman pod start podID diff --git a/cmd/podman/pods/stop.go b/cmd/podman/pods/stop.go index c8c3d2732..e8f82bee9 100644 --- a/cmd/podman/pods/stop.go +++ b/cmd/podman/pods/stop.go @@ -36,7 +36,7 @@ var ( Long: podStopDescription, RunE: stop, Args: func(cmd *cobra.Command, args []string) error { - return validate.CheckAllLatestAndPodIDFile(cmd, args, false, true) + return validate.CheckAllLatestAndIDFile(cmd, args, false, "pod-id-file") }, ValidArgsFunction: common.AutocompletePodsRunning, Example: `podman pod stop mywebserverpod diff --git a/cmd/podman/pods/unpause.go b/cmd/podman/pods/unpause.go index a308a82c3..8a0a24e98 100644 --- a/cmd/podman/pods/unpause.go +++ b/cmd/podman/pods/unpause.go @@ -22,7 +22,7 @@ var ( Long: podUnpauseDescription, RunE: unpause, Args: func(cmd *cobra.Command, args []string) error { - return validate.CheckAllLatestAndCIDFile(cmd, args, false, false) + return validate.CheckAllLatestAndIDFile(cmd, args, false, "") }, // TODO have a function which shows only pods which could be unpaused // for now show all diff --git a/cmd/podman/root.go b/cmd/podman/root.go index 2bd4fa723..1892ff9f7 100644 --- a/cmd/podman/root.go +++ b/cmd/podman/root.go @@ -423,7 +423,7 @@ func rootFlags(cmd *cobra.Command, opts *entities.PodmanConfig) { // -s is deprecated due to conflict with -s on subcommands storageDriverFlagName := "storage-driver" pFlags.StringVar(&opts.StorageDriver, storageDriverFlagName, "", "Select which storage driver is used to manage storage of images and containers") - _ = cmd.RegisterFlagCompletionFunc(storageDriverFlagName, completion.AutocompleteNone) //TODO: what can we recommend here? + _ = cmd.RegisterFlagCompletionFunc(storageDriverFlagName, completion.AutocompleteNone) tmpdirFlagName := "tmpdir" pFlags.StringVar(&opts.Engine.TmpDir, tmpdirFlagName, "", "Path to the tmp directory for libpod state content.\n\nNote: use the environment variable 'TMPDIR' to change the temporary storage location for container images, '/var/tmp'.\n") diff --git a/cmd/podman/system/reset.go b/cmd/podman/system/reset.go index 176573bf6..20f15a34f 100644 --- a/cmd/podman/system/reset.go +++ b/cmd/podman/system/reset.go @@ -91,18 +91,10 @@ func reset(cmd *cobra.Command, args []string) { registry.ContainerEngine().Shutdown(registry.Context()) registry.ImageEngine().Shutdown(registry.Context()) - engine, err := infra.NewSystemEngine(entities.ResetMode, registry.PodmanConfig()) - if err != nil { - logrus.Error(err) - os.Exit(define.ExecErrorCodeGeneric) - } - defer engine.Shutdown(registry.Context()) - - if err := engine.Reset(registry.Context()); err != nil { + // Do not try to shut the engine down, as a Reset engine is not valid + // after its creation. + if _, err := infra.NewSystemEngine(entities.ResetMode, registry.PodmanConfig()); err != nil { logrus.Error(err) - // FIXME change this to return the error like other commands - // defer will never run on os.Exit() - //nolint:gocritic os.Exit(define.ExecErrorCodeGeneric) } diff --git a/cmd/podman/system/unshare.go b/cmd/podman/system/unshare.go index 0ae5b81ad..1ed08eac3 100644 --- a/cmd/podman/system/unshare.go +++ b/cmd/podman/system/unshare.go @@ -2,10 +2,10 @@ package system import ( "os" - "os/exec" "github.com/containers/common/pkg/completion" "github.com/containers/podman/v4/cmd/podman/registry" + "github.com/containers/podman/v4/cmd/podman/utils" "github.com/containers/podman/v4/pkg/domain/entities" "github.com/containers/podman/v4/pkg/rootless" "github.com/pkg/errors" @@ -60,22 +60,5 @@ func unshare(cmd *cobra.Command, args []string) error { } err := registry.ContainerEngine().Unshare(registry.Context(), args, unshareOptions) - if err != nil { - if exitError, ok := err.(*exec.ExitError); ok { - // the user command inside the unshare env has failed - // we set the exit code, do not return the error to the user - // otherwise "exit status X" will be printed - registry.SetExitCode(exitError.ExitCode()) - return nil - } - // cmd.Run() can return fs.ErrNotExist, fs.ErrPermission or exec.ErrNotFound - // follow podman run/exec standard with the exit codes - if errors.Is(err, os.ErrNotExist) || errors.Is(err, exec.ErrNotFound) { - registry.SetExitCode(127) - } else if errors.Is(err, os.ErrPermission) { - registry.SetExitCode(126) - } - return err - } - return nil + return utils.HandleOSExecError(err) } diff --git a/cmd/podman/utils/error.go b/cmd/podman/utils/error.go index 2aaa71373..3efff0301 100644 --- a/cmd/podman/utils/error.go +++ b/cmd/podman/utils/error.go @@ -4,10 +4,12 @@ import ( "errors" "fmt" "os" + "os/exec" "strconv" "strings" buildahCLI "github.com/containers/buildah/pkg/cli" + "github.com/containers/podman/v4/cmd/podman/registry" ) type OutputErrors []error @@ -43,3 +45,33 @@ func ExitCodeFromBuildError(errorMsg string) (int, error) { } return buildahCLI.ExecErrorCodeGeneric, errors.New("message does not contains a valid exit code") } + +// HandleOSExecError checks the given error for an exec.ExitError error and +// sets the same podman exit code as the error. +// No error will be returned in this case to make sure things like podman +// unshare false work correctly without extra output. +// When the exec file does not exists we set the exit code to 127, for +// permission errors 126 is used as exit code. In this case we still return +// the error so the user gets an error message. +// If the error is nil it returns nil. +func HandleOSExecError(err error) error { + if err == nil { + return nil + } + var exitError *exec.ExitError + if errors.As(err, &exitError) { + // the user command inside the unshare/ssh env has failed + // we set the exit code, do not return the error to the user + // otherwise "exit status X" will be printed + registry.SetExitCode(exitError.ExitCode()) + return nil + } + // cmd.Run() can return fs.ErrNotExist, fs.ErrPermission or exec.ErrNotFound + // follow podman run/exec standard with the exit codes + if errors.Is(err, os.ErrNotExist) || errors.Is(err, exec.ErrNotFound) { + registry.SetExitCode(127) + } else if errors.Is(err, os.ErrPermission) { + registry.SetExitCode(126) + } + return err +} diff --git a/cmd/podman/utils/signals_linux.go b/cmd/podman/utils/signals_linux.go deleted file mode 100644 index dd0507c0e..000000000 --- a/cmd/podman/utils/signals_linux.go +++ /dev/null @@ -1,15 +0,0 @@ -//go:build !windows -// +build !windows - -package utils - -import ( - "os" - - "golang.org/x/sys/unix" -) - -// Platform specific signal synonyms -var ( - SIGHUP os.Signal = unix.SIGHUP -) diff --git a/cmd/podman/utils/signals_windows.go b/cmd/podman/utils/signals_windows.go deleted file mode 100644 index e6fcc1b32..000000000 --- a/cmd/podman/utils/signals_windows.go +++ /dev/null @@ -1,15 +0,0 @@ -//go:build windows -// +build windows - -package utils - -import ( - "os" - - "golang.org/x/sys/windows" -) - -// Platform specific signal synonyms -var ( - SIGHUP os.Signal = windows.SIGHUP -) diff --git a/cmd/podman/validate/args.go b/cmd/podman/validate/args.go index 669456bd3..b9b468d34 100644 --- a/cmd/podman/validate/args.go +++ b/cmd/podman/validate/args.go @@ -50,89 +50,44 @@ func IDOrLatestArgs(cmd *cobra.Command, args []string) error { return nil } -// TODO: the two functions CheckAllLatestAndCIDFile and CheckAllLatestAndPodIDFile are almost identical. -// It may be worth looking into generalizing the two a bit more and share code but time is scarce and -// we only live once. - -// CheckAllLatestAndCIDFile checks that --all and --latest are used correctly. -// If cidfile is set, also check for the --cidfile flag. +// CheckAllLatestAndCIDFile checks that --all and --latest are used correctly for containers and pods +// If idFileFlag is set is set, also checks for the --cidfile or --pod-id-file flag. +// Note: this has been deprecated, use CheckAllLatestAndIDFile instead func CheckAllLatestAndCIDFile(c *cobra.Command, args []string, ignoreArgLen bool, cidfile bool) error { - var specifiedLatest bool - argLen := len(args) - if !registry.IsRemote() { - specifiedLatest, _ = c.Flags().GetBool("latest") - if c.Flags().Lookup("all") == nil || c.Flags().Lookup("latest") == nil { - if !cidfile { - return errors.New("unable to lookup values for 'latest' or 'all'") - } else if c.Flags().Lookup("cidfile") == nil { - return errors.New("unable to lookup values for 'latest', 'all' or 'cidfile'") - } - } - } - - specifiedAll, _ := c.Flags().GetBool("all") - specifiedCIDFile := false - if cid, _ := c.Flags().GetStringArray("cidfile"); len(cid) > 0 { - specifiedCIDFile = true - } - - if specifiedCIDFile && (specifiedAll || specifiedLatest) { - return errors.Errorf("--all, --latest and --cidfile cannot be used together") - } else if specifiedAll && specifiedLatest { - return errors.Errorf("--all and --latest cannot be used together") - } - - if (argLen > 0) && specifiedAll { - return errors.Errorf("no arguments are needed with --all") - } - - if ignoreArgLen { - return nil - } - - if argLen > 0 { - if specifiedLatest { - return errors.Errorf("--latest and containers cannot be used together") - } else if cidfile && (specifiedLatest || specifiedCIDFile) { - return errors.Errorf("no arguments are needed with --latest or --cidfile") - } - } - - if specifiedCIDFile { - return nil - } - - if argLen < 1 && !specifiedAll && !specifiedLatest && !specifiedCIDFile { - return errors.Errorf("you must provide at least one name or id") - } - return nil + return CheckAllLatestAndIDFile(c, args, ignoreArgLen, "cidfile") } // CheckAllLatestAndPodIDFile checks that --all and --latest are used correctly. // If withIDFile is set, also check for the --pod-id-file flag. +// Note: this has been deprecated, use CheckAllLatestAndIDFile instead func CheckAllLatestAndPodIDFile(c *cobra.Command, args []string, ignoreArgLen bool, withIDFile bool) error { + return CheckAllLatestAndIDFile(c, args, ignoreArgLen, "pod-id-file") +} + +// CheckAllLatestAndIDFile checks that --all and --latest are used correctly for containers and pods +// If idFileFlag is set is set, also checks for the --cidfile or --pod-id-file flag. +func CheckAllLatestAndIDFile(c *cobra.Command, args []string, ignoreArgLen bool, idFileFlag string) error { var specifiedLatest bool argLen := len(args) if !registry.IsRemote() { - // remote clients have no latest flag specifiedLatest, _ = c.Flags().GetBool("latest") if c.Flags().Lookup("all") == nil || c.Flags().Lookup("latest") == nil { - if !withIDFile { + if idFileFlag == "" { return errors.New("unable to lookup values for 'latest' or 'all'") - } else if c.Flags().Lookup("pod-id-file") == nil { - return errors.New("unable to lookup values for 'latest', 'all' or 'pod-id-file'") + } else if c.Flags().Lookup(idFileFlag) == nil { + return errors.Errorf("unable to lookup values for 'latest', 'all', or '%s'", idFileFlag) } } } specifiedAll, _ := c.Flags().GetBool("all") - specifiedPodIDFile := false - if pid, _ := c.Flags().GetStringArray("pod-id-file"); len(pid) > 0 { - specifiedPodIDFile = true + specifiedIDFile := false + if cid, _ := c.Flags().GetStringArray(idFileFlag); len(cid) > 0 { + specifiedIDFile = true } - if specifiedPodIDFile && (specifiedAll || specifiedLatest) { - return errors.Errorf("--all, --latest and --pod-id-file cannot be used together") + if specifiedIDFile && (specifiedAll || specifiedLatest) { + return errors.Errorf("--all, --latest, and --%s cannot be used together", idFileFlag) } else if specifiedAll && specifiedLatest { return errors.Errorf("--all and --latest cannot be used together") } @@ -147,17 +102,17 @@ func CheckAllLatestAndPodIDFile(c *cobra.Command, args []string, ignoreArgLen bo if argLen > 0 { if specifiedLatest { - return errors.Errorf("--latest and pods cannot be used together") - } else if withIDFile && (specifiedLatest || specifiedPodIDFile) { - return errors.Errorf("no arguments are needed with --latest or --pod-id-file") + return errors.Errorf("--latest and containers cannot be used together") + } else if idFileFlag != "" && (specifiedLatest || specifiedIDFile) { + return errors.Errorf("no arguments are needed with --latest or --%s", idFileFlag) } } - if specifiedPodIDFile { + if specifiedIDFile { return nil } - if argLen < 1 && !specifiedAll && !specifiedLatest && !specifiedPodIDFile { + if argLen < 1 && !specifiedAll && !specifiedLatest && !specifiedIDFile { return errors.Errorf("you must provide at least one name or id") } return nil diff --git a/cmd/podman/volumes/create.go b/cmd/podman/volumes/create.go index 1668c72de..b47ae16ce 100644 --- a/cmd/podman/volumes/create.go +++ b/cmd/podman/volumes/create.go @@ -17,6 +17,7 @@ var ( createCommand = &cobra.Command{ Use: "create [options] [NAME]", + Args: cobra.MaximumNArgs(1), Short: "Create a new volume", Long: createDescription, RunE: create, @@ -59,9 +60,6 @@ func create(cmd *cobra.Command, args []string) error { var ( err error ) - if len(args) > 1 { - return errors.Errorf("too many arguments, create takes at most 1 argument") - } if len(args) > 0 { createOpts.Name = args[0] } diff --git a/cmd/podman/volumes/export.go b/cmd/podman/volumes/export.go index 1011604de..113f79a0b 100644 --- a/cmd/podman/volumes/export.go +++ b/cmd/podman/volumes/export.go @@ -6,9 +6,9 @@ import ( "github.com/containers/common/pkg/completion" "github.com/containers/podman/v4/cmd/podman/common" - "github.com/containers/podman/v4/cmd/podman/inspect" "github.com/containers/podman/v4/cmd/podman/registry" "github.com/containers/podman/v4/pkg/domain/entities" + "github.com/containers/podman/v4/pkg/errorhandling" "github.com/containers/podman/v4/utils" "github.com/pkg/errors" "github.com/sirupsen/logrus" @@ -58,11 +58,14 @@ func export(cmd *cobra.Command, args []string) error { if cliExportOpts.Output == "" { return errors.New("expects output path, use --output=[path]") } - inspectOpts.Type = inspect.VolumeType - volumeData, _, err := containerEngine.VolumeInspect(ctx, args, inspectOpts) + inspectOpts.Type = common.VolumeType + volumeData, errs, err := containerEngine.VolumeInspect(ctx, args, inspectOpts) if err != nil { return err } + if len(errs) > 0 { + return errorhandling.JoinErrors(errs) + } if len(volumeData) < 1 { return errors.New("no volume data found") } diff --git a/cmd/podman/volumes/import.go b/cmd/podman/volumes/import.go index 9ff17e5b1..76a311643 100644 --- a/cmd/podman/volumes/import.go +++ b/cmd/podman/volumes/import.go @@ -5,10 +5,10 @@ import ( "os" "github.com/containers/podman/v4/cmd/podman/common" - "github.com/containers/podman/v4/cmd/podman/inspect" "github.com/containers/podman/v4/cmd/podman/parse" "github.com/containers/podman/v4/cmd/podman/registry" "github.com/containers/podman/v4/pkg/domain/entities" + "github.com/containers/podman/v4/pkg/errorhandling" "github.com/containers/podman/v4/utils" "github.com/pkg/errors" "github.com/spf13/cobra" @@ -60,11 +60,15 @@ func importVol(cmd *cobra.Command, args []string) error { tarFile = os.Stdin } - inspectOpts.Type = inspect.VolumeType - volumeData, _, err := containerEngine.VolumeInspect(ctx, volumes, inspectOpts) + inspectOpts.Type = common.VolumeType + inspectOpts.Type = common.VolumeType + volumeData, errs, err := containerEngine.VolumeInspect(ctx, volumes, inspectOpts) if err != nil { return err } + if len(errs) > 0 { + return errorhandling.JoinErrors(errs) + } if len(volumeData) < 1 { return errors.New("no volume data found") } diff --git a/cmd/podman/volumes/inspect.go b/cmd/podman/volumes/inspect.go index f21f9c233..7cf363f36 100644 --- a/cmd/podman/volumes/inspect.go +++ b/cmd/podman/volumes/inspect.go @@ -48,6 +48,6 @@ func volumeInspect(cmd *cobra.Command, args []string) error { if (inspectOpts.All && len(args) > 0) || (!inspectOpts.All && len(args) < 1) { return errors.New("provide one or more volume names or use --all") } - inspectOpts.Type = inspect.VolumeType + inspectOpts.Type = common.VolumeType return inspect.Inspect(args, *inspectOpts) } |