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  }