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