github.com/tilt-dev/tilt@v0.33.15-0.20240515162809-0a22ed45d8a0/internal/tiltfile/config/config_def.go (about)

     1  package config
     2  
     3  import (
     4  	"bytes"
     5  	"fmt"
     6  	"os"
     7  	"strings"
     8  
     9  	jsoniter "github.com/json-iterator/go"
    10  	"github.com/pkg/errors"
    11  	flag "github.com/spf13/pflag"
    12  	"go.starlark.net/starlark"
    13  
    14  	"github.com/tilt-dev/tilt/internal/tiltfile/starkit"
    15  )
    16  
    17  type configValue interface {
    18  	flag.Value
    19  	starlark() starlark.Value
    20  	setFromInterface(interface{}) error
    21  	IsSet() bool
    22  }
    23  
    24  type configMap map[string]configValue
    25  
    26  type configSetting struct {
    27  	newValue func() configValue
    28  	usage    string
    29  }
    30  
    31  type ConfigDef struct {
    32  	positionalSettingName string
    33  	configSettings        map[string]configSetting
    34  }
    35  
    36  func (cm configMap) toStarlark() (starlark.Mapping, error) {
    37  	ret := starlark.NewDict(len(cm))
    38  	for k, v := range cm {
    39  		err := ret.SetKey(starlark.String(k), v.starlark())
    40  		if err != nil {
    41  			return nil, err
    42  		}
    43  	}
    44  	return ret, nil
    45  }
    46  
    47  // merges settings from config and settings from args, with settings from args trumping
    48  func mergeConfigMaps(settingsFromConfig, settingsFromArgs configMap) configMap {
    49  	ret := make(configMap)
    50  	for k, v := range settingsFromConfig {
    51  		ret[k] = v
    52  	}
    53  
    54  	for k, v := range settingsFromArgs {
    55  		if v.IsSet() {
    56  			ret[k] = v
    57  		}
    58  	}
    59  
    60  	return ret
    61  }
    62  
    63  // parse any args and merge them into the config
    64  func (cd ConfigDef) incorporateArgs(config configMap, args []string) (ret configMap, output string, err error) {
    65  	var settingsFromArgs configMap
    66  	settingsFromArgs, output, err = cd.parseArgs(args)
    67  	if err != nil {
    68  		return nil, output, fmt.Errorf("invalid Tiltfile config args: %v", err)
    69  	}
    70  
    71  	config = mergeConfigMaps(config, settingsFromArgs)
    72  
    73  	return config, output, nil
    74  }
    75  
    76  func (cd ConfigDef) parse(configPath string, args []string) (v starlark.Value, output string, err error) {
    77  	config, err := cd.readFromFile(configPath)
    78  	if err != nil {
    79  		return starlark.None, "", err
    80  	}
    81  
    82  	config, output, err = cd.incorporateArgs(config, args)
    83  	if err != nil {
    84  		return starlark.None, output, err
    85  	}
    86  
    87  	ret, err := config.toStarlark()
    88  	if err != nil {
    89  		return nil, output, err
    90  	}
    91  
    92  	return ret, output, nil
    93  }
    94  
    95  // parse command-line args
    96  func (cd ConfigDef) parseArgs(args []string) (ret configMap, output string, err error) {
    97  	fs := flag.NewFlagSet("", flag.ContinueOnError)
    98  	w := &bytes.Buffer{}
    99  	fs.SetOutput(w)
   100  
   101  	ret = make(configMap)
   102  	for name, def := range cd.configSettings {
   103  		ret[name] = def.newValue()
   104  		if name == cd.positionalSettingName {
   105  			continue
   106  		}
   107  		fs.Var(ret[name], name, def.usage)
   108  		// for bools, make "--foo" equal to "--foo true"
   109  		if _, ok := ret[name].(*boolSetting); ok {
   110  			fs.Lookup(name).NoOptDefVal = "true"
   111  		}
   112  	}
   113  
   114  	err = fs.Parse(args)
   115  	if err != nil {
   116  		usage := fs.FlagUsagesWrapped(80)
   117  		if strings.TrimSpace(usage) != "" {
   118  			usage = "\nUsage:\n" + usage
   119  		}
   120  		return nil, w.String(), fmt.Errorf("%v%s", err, usage)
   121  	}
   122  
   123  	if len(fs.Args()) > 0 {
   124  		if cd.positionalSettingName == "" {
   125  			return nil, w.String(), fmt.Errorf(
   126  				"positional CLI args (%q) were specified, but none were expected.\n"+
   127  					"See https://docs.tilt.dev/tiltfile_config.html#positional-arguments for examples.", strings.Join(fs.Args(), " "))
   128  		} else {
   129  			for _, arg := range fs.Args() {
   130  				err := ret[cd.positionalSettingName].Set(arg)
   131  				if err != nil {
   132  					return nil, w.String(), fmt.Errorf("invalid positional args (%s): %v", cd.positionalSettingName, err)
   133  				}
   134  			}
   135  		}
   136  	}
   137  
   138  	return ret, w.String(), nil
   139  }
   140  
   141  // parse settings from the config file
   142  func (cd ConfigDef) readFromFile(tiltConfigPath string) (ret configMap, err error) {
   143  	ret = make(configMap)
   144  	r, err := os.Open(tiltConfigPath)
   145  	if err != nil {
   146  		if os.IsNotExist(err) {
   147  			return ret, nil
   148  		}
   149  		return nil, errors.Wrapf(err, "error opening %s", tiltConfigPath)
   150  	}
   151  	defer func() {
   152  		_ = r.Close()
   153  	}()
   154  
   155  	m := make(map[string]interface{})
   156  	err = jsoniter.NewDecoder(r).Decode(&m)
   157  	if err != nil {
   158  		return nil, errors.Wrapf(err, "error parsing json from %s", tiltConfigPath)
   159  	}
   160  
   161  	for k, v := range m {
   162  		def, ok := cd.configSettings[k]
   163  		if !ok {
   164  			return nil, fmt.Errorf("%s specified unknown setting name '%s'", tiltConfigPath, k)
   165  		}
   166  		ret[k] = def.newValue()
   167  		err = ret[k].setFromInterface(v)
   168  		if err != nil {
   169  			return nil, errors.Wrapf(err, "%s specified invalid value for setting %s", tiltConfigPath, k)
   170  		}
   171  	}
   172  	return ret, nil
   173  }
   174  
   175  // makes a new builtin with the given configValue constructor
   176  // newConfigValue: a constructor for the `configValue` that we're making a function for
   177  //
   178  //	(it's the same logic for all types, except for the `configValue` that gets saved)
   179  func configSettingDefinitionBuiltin(newConfigValue func() configValue) starkit.Function {
   180  	return func(thread *starlark.Thread, fn *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {
   181  		var name string
   182  		var isArgs bool
   183  		var usage string
   184  		err := starkit.UnpackArgs(thread, fn.Name(), args, kwargs,
   185  			"name",
   186  			&name,
   187  			"args?",
   188  			&isArgs,
   189  			"usage?",
   190  			&usage,
   191  		)
   192  		if err != nil {
   193  			return starlark.None, err
   194  		}
   195  
   196  		if name == "" {
   197  			return starlark.None, errors.New("'name' is required")
   198  		}
   199  
   200  		err = starkit.SetState(thread, func(settings Settings) (Settings, error) {
   201  			if settings.configParseCalled {
   202  				return settings, fmt.Errorf("%s cannot be called after config.parse is called", fn.Name())
   203  			}
   204  
   205  			if _, ok := settings.configDef.configSettings[name]; ok {
   206  				return settings, fmt.Errorf("%s defined multiple times", name)
   207  			}
   208  
   209  			if isArgs {
   210  				if settings.configDef.positionalSettingName != "" {
   211  					return settings, fmt.Errorf("both %s and %s are defined as positional args", name, settings.configDef.positionalSettingName)
   212  				}
   213  
   214  				settings.configDef.positionalSettingName = name
   215  			}
   216  
   217  			settings.configDef.configSettings[name] = configSetting{
   218  				newValue: newConfigValue,
   219  				usage:    usage,
   220  			}
   221  
   222  			return settings, nil
   223  		})
   224  		if err != nil {
   225  			return starlark.None, err
   226  		}
   227  
   228  		return starlark.None, nil
   229  	}
   230  }