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