github.1git.de/docker/cli@v26.1.3+incompatible/cli/command/utils.go (about) 1 // FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16: 2 //go:build go1.19 3 4 package command 5 6 import ( 7 "bufio" 8 "context" 9 "fmt" 10 "io" 11 "os" 12 "os/signal" 13 "path/filepath" 14 "runtime" 15 "strings" 16 "syscall" 17 18 "github.com/docker/cli/cli/streams" 19 "github.com/docker/docker/api/types/filters" 20 mounttypes "github.com/docker/docker/api/types/mount" 21 "github.com/docker/docker/api/types/versions" 22 "github.com/docker/docker/errdefs" 23 "github.com/moby/sys/sequential" 24 "github.com/pkg/errors" 25 "github.com/spf13/pflag" 26 ) 27 28 // CopyToFile writes the content of the reader to the specified file 29 func CopyToFile(outfile string, r io.Reader) error { 30 // We use sequential file access here to avoid depleting the standby list 31 // on Windows. On Linux, this is a call directly to os.CreateTemp 32 tmpFile, err := sequential.CreateTemp(filepath.Dir(outfile), ".docker_temp_") 33 if err != nil { 34 return err 35 } 36 37 tmpPath := tmpFile.Name() 38 39 _, err = io.Copy(tmpFile, r) 40 tmpFile.Close() 41 42 if err != nil { 43 os.Remove(tmpPath) 44 return err 45 } 46 47 if err = os.Rename(tmpPath, outfile); err != nil { 48 os.Remove(tmpPath) 49 return err 50 } 51 52 return nil 53 } 54 55 // capitalizeFirst capitalizes the first character of string 56 func capitalizeFirst(s string) string { 57 switch l := len(s); l { 58 case 0: 59 return s 60 case 1: 61 return strings.ToLower(s) 62 default: 63 return strings.ToUpper(string(s[0])) + strings.ToLower(s[1:]) 64 } 65 } 66 67 // PrettyPrint outputs arbitrary data for human formatted output by uppercasing the first letter. 68 func PrettyPrint(i any) string { 69 switch t := i.(type) { 70 case nil: 71 return "None" 72 case string: 73 return capitalizeFirst(t) 74 default: 75 return capitalizeFirst(fmt.Sprintf("%s", t)) 76 } 77 } 78 79 var ErrPromptTerminated = errdefs.Cancelled(errors.New("prompt terminated")) 80 81 // PromptForConfirmation requests and checks confirmation from the user. 82 // This will display the provided message followed by ' [y/N] '. If the user 83 // input 'y' or 'Y' it returns true otherwise false. If no message is provided, 84 // "Are you sure you want to proceed? [y/N] " will be used instead. 85 // 86 // If the user terminates the CLI with SIGINT or SIGTERM while the prompt is 87 // active, the prompt will return false with an ErrPromptTerminated error. 88 // When the prompt returns an error, the caller should propagate the error up 89 // the stack and close the io.Reader used for the prompt which will prevent the 90 // background goroutine from blocking indefinitely. 91 func PromptForConfirmation(ctx context.Context, ins io.Reader, outs io.Writer, message string) (bool, error) { 92 if message == "" { 93 message = "Are you sure you want to proceed?" 94 } 95 message += " [y/N] " 96 97 _, _ = fmt.Fprint(outs, message) 98 99 // On Windows, force the use of the regular OS stdin stream. 100 if runtime.GOOS == "windows" { 101 ins = streams.NewIn(os.Stdin) 102 } 103 104 result := make(chan bool) 105 106 // Catch the termination signal and exit the prompt gracefully. 107 // The caller is responsible for properly handling the termination. 108 notifyCtx, notifyCancel := signal.NotifyContext(ctx, syscall.SIGINT, syscall.SIGTERM) 109 defer notifyCancel() 110 111 go func() { 112 var res bool 113 scanner := bufio.NewScanner(ins) 114 if scanner.Scan() { 115 answer := strings.TrimSpace(scanner.Text()) 116 if strings.EqualFold(answer, "y") { 117 res = true 118 } 119 } 120 result <- res 121 }() 122 123 select { 124 case <-notifyCtx.Done(): 125 // print a newline on termination 126 _, _ = fmt.Fprintln(outs, "") 127 return false, ErrPromptTerminated 128 case r := <-result: 129 return r, nil 130 } 131 } 132 133 // PruneFilters returns consolidated prune filters obtained from config.json and cli 134 func PruneFilters(dockerCli Cli, pruneFilters filters.Args) filters.Args { 135 if dockerCli.ConfigFile() == nil { 136 return pruneFilters 137 } 138 for _, f := range dockerCli.ConfigFile().PruneFilters { 139 k, v, ok := strings.Cut(f, "=") 140 if !ok { 141 continue 142 } 143 if k == "label" { 144 // CLI label filter supersede config.json. 145 // If CLI label filter conflict with config.json, 146 // skip adding label! filter in config.json. 147 if pruneFilters.Contains("label!") && pruneFilters.ExactMatch("label!", v) { 148 continue 149 } 150 } else if k == "label!" { 151 // CLI label! filter supersede config.json. 152 // If CLI label! filter conflict with config.json, 153 // skip adding label filter in config.json. 154 if pruneFilters.Contains("label") && pruneFilters.ExactMatch("label", v) { 155 continue 156 } 157 } 158 pruneFilters.Add(k, v) 159 } 160 161 return pruneFilters 162 } 163 164 // AddPlatformFlag adds `platform` to a set of flags for API version 1.32 and later. 165 func AddPlatformFlag(flags *pflag.FlagSet, target *string) { 166 flags.StringVar(target, "platform", os.Getenv("DOCKER_DEFAULT_PLATFORM"), "Set platform if server is multi-platform capable") 167 flags.SetAnnotation("platform", "version", []string{"1.32"}) 168 } 169 170 // ValidateOutputPath validates the output paths of the `export` and `save` commands. 171 func ValidateOutputPath(path string) error { 172 dir := filepath.Dir(filepath.Clean(path)) 173 if dir != "" && dir != "." { 174 if _, err := os.Stat(dir); os.IsNotExist(err) { 175 return errors.Errorf("invalid output path: directory %q does not exist", dir) 176 } 177 } 178 // check whether `path` points to a regular file 179 // (if the path exists and doesn't point to a directory) 180 if fileInfo, err := os.Stat(path); !os.IsNotExist(err) { 181 if err != nil { 182 return err 183 } 184 185 if fileInfo.Mode().IsDir() || fileInfo.Mode().IsRegular() { 186 return nil 187 } 188 189 if err := ValidateOutputPathFileMode(fileInfo.Mode()); err != nil { 190 return errors.Wrapf(err, fmt.Sprintf("invalid output path: %q must be a directory or a regular file", path)) 191 } 192 } 193 return nil 194 } 195 196 // ValidateOutputPathFileMode validates the output paths of the `cp` command and serves as a 197 // helper to `ValidateOutputPath` 198 func ValidateOutputPathFileMode(fileMode os.FileMode) error { 199 switch { 200 case fileMode&os.ModeDevice != 0: 201 return errors.New("got a device") 202 case fileMode&os.ModeIrregular != 0: 203 return errors.New("got an irregular file") 204 } 205 return nil 206 } 207 208 func stringSliceIndex(s, subs []string) int { 209 j := 0 210 if len(subs) > 0 { 211 for i, x := range s { 212 if j < len(subs) && subs[j] == x { 213 j++ 214 } else { 215 j = 0 216 } 217 if len(subs) == j { 218 return i + 1 - j 219 } 220 } 221 } 222 return -1 223 } 224 225 // StringSliceReplaceAt replaces the sub-slice find, with the sub-slice replace, in the string 226 // slice s, returning a new slice and a boolean indicating if the replacement happened. 227 // requireIdx is the index at which old needs to be found at (or -1 to disregard that). 228 func StringSliceReplaceAt(s, find, replace []string, requireIndex int) ([]string, bool) { 229 idx := stringSliceIndex(s, find) 230 if (requireIndex != -1 && requireIndex != idx) || idx == -1 { 231 return s, false 232 } 233 out := append([]string{}, s[:idx]...) 234 out = append(out, replace...) 235 out = append(out, s[idx+len(find):]...) 236 return out, true 237 } 238 239 // ValidateMountWithAPIVersion validates a mount with the server API version. 240 func ValidateMountWithAPIVersion(m mounttypes.Mount, serverAPIVersion string) error { 241 if m.BindOptions != nil { 242 if m.BindOptions.NonRecursive && versions.LessThan(serverAPIVersion, "1.40") { 243 return errors.Errorf("bind-recursive=disabled requires API v1.40 or later") 244 } 245 // ReadOnlyNonRecursive can be safely ignored when API < 1.44 246 if m.BindOptions.ReadOnlyForceRecursive && versions.LessThan(serverAPIVersion, "1.44") { 247 return errors.Errorf("bind-recursive=readonly requires API v1.44 or later") 248 } 249 } 250 return nil 251 }