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  }