github.com/khulnasoft/cli@v0.0.0-20240402070845-01bcad7beefa/opts/mount.go (about)

     1  package opts
     2  
     3  import (
     4  	"encoding/csv"
     5  	"errors"
     6  	"fmt"
     7  	"os"
     8  	"path/filepath"
     9  	"strconv"
    10  	"strings"
    11  
    12  	mounttypes "github.com/docker/docker/api/types/mount"
    13  	"github.com/khulnasoft-lab/go-units"
    14  	"github.com/sirupsen/logrus"
    15  )
    16  
    17  // MountOpt is a Value type for parsing mounts
    18  type MountOpt struct {
    19  	values []mounttypes.Mount
    20  }
    21  
    22  // Set a new mount value
    23  //
    24  //nolint:gocyclo
    25  func (m *MountOpt) Set(value string) error {
    26  	csvReader := csv.NewReader(strings.NewReader(value))
    27  	fields, err := csvReader.Read()
    28  	if err != nil {
    29  		return err
    30  	}
    31  
    32  	mount := mounttypes.Mount{}
    33  
    34  	volumeOptions := func() *mounttypes.VolumeOptions {
    35  		if mount.VolumeOptions == nil {
    36  			mount.VolumeOptions = &mounttypes.VolumeOptions{
    37  				Labels: make(map[string]string),
    38  			}
    39  		}
    40  		if mount.VolumeOptions.DriverConfig == nil {
    41  			mount.VolumeOptions.DriverConfig = &mounttypes.Driver{}
    42  		}
    43  		return mount.VolumeOptions
    44  	}
    45  
    46  	bindOptions := func() *mounttypes.BindOptions {
    47  		if mount.BindOptions == nil {
    48  			mount.BindOptions = new(mounttypes.BindOptions)
    49  		}
    50  		return mount.BindOptions
    51  	}
    52  
    53  	tmpfsOptions := func() *mounttypes.TmpfsOptions {
    54  		if mount.TmpfsOptions == nil {
    55  			mount.TmpfsOptions = new(mounttypes.TmpfsOptions)
    56  		}
    57  		return mount.TmpfsOptions
    58  	}
    59  
    60  	setValueOnMap := func(target map[string]string, value string) {
    61  		k, v, _ := strings.Cut(value, "=")
    62  		if k != "" {
    63  			target[k] = v
    64  		}
    65  	}
    66  
    67  	mount.Type = mounttypes.TypeVolume // default to volume mounts
    68  	// Set writable as the default
    69  	for _, field := range fields {
    70  		key, val, ok := strings.Cut(field, "=")
    71  
    72  		// TODO(thaJeztah): these options should not be case-insensitive.
    73  		key = strings.ToLower(key)
    74  
    75  		if !ok {
    76  			switch key {
    77  			case "readonly", "ro":
    78  				mount.ReadOnly = true
    79  				continue
    80  			case "volume-nocopy":
    81  				volumeOptions().NoCopy = true
    82  				continue
    83  			case "bind-nonrecursive":
    84  				bindOptions().NonRecursive = true
    85  				continue
    86  			default:
    87  				return fmt.Errorf("invalid field '%s' must be a key=value pair", field)
    88  			}
    89  		}
    90  
    91  		switch key {
    92  		case "type":
    93  			mount.Type = mounttypes.Type(strings.ToLower(val))
    94  		case "source", "src":
    95  			mount.Source = val
    96  			if strings.HasPrefix(val, "."+string(filepath.Separator)) || val == "." {
    97  				if abs, err := filepath.Abs(val); err == nil {
    98  					mount.Source = abs
    99  				}
   100  			}
   101  		case "target", "dst", "destination":
   102  			mount.Target = val
   103  		case "readonly", "ro":
   104  			mount.ReadOnly, err = strconv.ParseBool(val)
   105  			if err != nil {
   106  				return fmt.Errorf("invalid value for %s: %s", key, val)
   107  			}
   108  		case "consistency":
   109  			mount.Consistency = mounttypes.Consistency(strings.ToLower(val))
   110  		case "bind-propagation":
   111  			bindOptions().Propagation = mounttypes.Propagation(strings.ToLower(val))
   112  		case "bind-nonrecursive":
   113  			bindOptions().NonRecursive, err = strconv.ParseBool(val)
   114  			if err != nil {
   115  				return fmt.Errorf("invalid value for %s: %s", key, val)
   116  			}
   117  			logrus.Warn("bind-nonrecursive is deprecated, use bind-recursive=disabled instead")
   118  		case "bind-recursive":
   119  			switch val {
   120  			case "enabled": // read-only mounts are recursively read-only if Engine >= v25 && kernel >= v5.12, otherwise writable
   121  				// NOP
   122  			case "disabled": // alias of bind-nonrecursive=true
   123  				bindOptions().NonRecursive = true
   124  			case "writable": // conforms to the default read-only bind-mount of Docker v24; read-only mounts are recursively mounted but not recursively read-only
   125  				bindOptions().ReadOnlyNonRecursive = true
   126  			case "readonly": // force recursively read-only, or raise an error
   127  				bindOptions().ReadOnlyForceRecursive = true
   128  				// TODO: implicitly set propagation and error if the user specifies a propagation in a future refactor/UX polish pass
   129  				// https://github.com/khulnasoft/cli/pull/4316#discussion_r1341974730
   130  			default:
   131  				return fmt.Errorf("invalid value for %s: %s (must be \"enabled\", \"disabled\", \"writable\", or \"readonly\")",
   132  					key, val)
   133  			}
   134  		case "volume-subpath":
   135  			volumeOptions().Subpath = val
   136  		case "volume-nocopy":
   137  			volumeOptions().NoCopy, err = strconv.ParseBool(val)
   138  			if err != nil {
   139  				return fmt.Errorf("invalid value for volume-nocopy: %s", val)
   140  			}
   141  		case "volume-label":
   142  			setValueOnMap(volumeOptions().Labels, val)
   143  		case "volume-driver":
   144  			volumeOptions().DriverConfig.Name = val
   145  		case "volume-opt":
   146  			if volumeOptions().DriverConfig.Options == nil {
   147  				volumeOptions().DriverConfig.Options = make(map[string]string)
   148  			}
   149  			setValueOnMap(volumeOptions().DriverConfig.Options, val)
   150  		case "tmpfs-size":
   151  			sizeBytes, err := units.RAMInBytes(val)
   152  			if err != nil {
   153  				return fmt.Errorf("invalid value for %s: %s", key, val)
   154  			}
   155  			tmpfsOptions().SizeBytes = sizeBytes
   156  		case "tmpfs-mode":
   157  			ui64, err := strconv.ParseUint(val, 8, 32)
   158  			if err != nil {
   159  				return fmt.Errorf("invalid value for %s: %s", key, val)
   160  			}
   161  			tmpfsOptions().Mode = os.FileMode(ui64)
   162  		default:
   163  			return fmt.Errorf("unexpected key '%s' in '%s'", key, field)
   164  		}
   165  	}
   166  
   167  	if mount.Type == "" {
   168  		return fmt.Errorf("type is required")
   169  	}
   170  
   171  	if mount.Target == "" {
   172  		return fmt.Errorf("target is required")
   173  	}
   174  
   175  	if mount.VolumeOptions != nil && mount.Type != mounttypes.TypeVolume {
   176  		return fmt.Errorf("cannot mix 'volume-*' options with mount type '%s'", mount.Type)
   177  	}
   178  	if mount.BindOptions != nil && mount.Type != mounttypes.TypeBind {
   179  		return fmt.Errorf("cannot mix 'bind-*' options with mount type '%s'", mount.Type)
   180  	}
   181  	if mount.TmpfsOptions != nil && mount.Type != mounttypes.TypeTmpfs {
   182  		return fmt.Errorf("cannot mix 'tmpfs-*' options with mount type '%s'", mount.Type)
   183  	}
   184  
   185  	if mount.BindOptions != nil {
   186  		if mount.BindOptions.ReadOnlyNonRecursive {
   187  			if !mount.ReadOnly {
   188  				return errors.New("option 'bind-recursive=writable' requires 'readonly' to be specified in conjunction")
   189  			}
   190  		}
   191  		if mount.BindOptions.ReadOnlyForceRecursive {
   192  			if !mount.ReadOnly {
   193  				return errors.New("option 'bind-recursive=readonly' requires 'readonly' to be specified in conjunction")
   194  			}
   195  			if mount.BindOptions.Propagation != mounttypes.PropagationRPrivate {
   196  				return errors.New("option 'bind-recursive=readonly' requires 'bind-propagation=rprivate' to be specified in conjunction")
   197  			}
   198  		}
   199  	}
   200  
   201  	m.values = append(m.values, mount)
   202  	return nil
   203  }
   204  
   205  // Type returns the type of this option
   206  func (m *MountOpt) Type() string {
   207  	return "mount"
   208  }
   209  
   210  // String returns a string repr of this option
   211  func (m *MountOpt) String() string {
   212  	mounts := []string{}
   213  	for _, mount := range m.values {
   214  		repr := fmt.Sprintf("%s %s %s", mount.Type, mount.Source, mount.Target)
   215  		mounts = append(mounts, repr)
   216  	}
   217  	return strings.Join(mounts, ", ")
   218  }
   219  
   220  // Value returns the mounts
   221  func (m *MountOpt) Value() []mounttypes.Mount {
   222  	return m.values
   223  }