github.com/juju/charm/v11@v11.2.0/config.go (about)

     1  // Copyright 2011, 2012, 2013 Canonical Ltd.
     2  // Licensed under the LGPLv3, see LICENCE file for details.
     3  
     4  package charm
     5  
     6  import (
     7  	"fmt"
     8  	"io"
     9  	"io/ioutil"
    10  	"net/url"
    11  	"strconv"
    12  
    13  	"github.com/juju/errors"
    14  	"github.com/juju/schema"
    15  	"gopkg.in/yaml.v2"
    16  )
    17  
    18  // Settings is a group of charm config option names and values. A Settings
    19  // S is considered valid by the Config C if every key in S is an option in
    20  // C, and every value either has the correct type or is nil.
    21  type Settings map[string]interface{}
    22  
    23  // Option represents a single charm config option.
    24  type Option struct {
    25  	Type        string      `yaml:"type"`
    26  	Description string      `yaml:"description,omitempty"`
    27  	Default     interface{} `yaml:"default,omitempty"`
    28  }
    29  
    30  // error replaces any supplied non-nil error with a new error describing a
    31  // validation failure for the supplied value.
    32  func (option Option) error(err *error, name string, value interface{}) {
    33  	if *err != nil {
    34  		*err = fmt.Errorf("option %q expected %s, got %#v", name, option.Type, value)
    35  	}
    36  }
    37  
    38  const secretScheme = "secret"
    39  
    40  type secretC struct{}
    41  
    42  // Coerce implements schema.Checker.Coerce for secretC.
    43  func (c secretC) Coerce(v interface{}, path []string) (interface{}, error) {
    44  	s, err := schema.String().Coerce(v, path)
    45  	if err != nil {
    46  		return nil, err
    47  	}
    48  	str := s.(string)
    49  	if str == "" {
    50  		return "", nil
    51  	}
    52  	u, err := url.Parse(str)
    53  	if err != nil {
    54  		return nil, errors.Trace(err)
    55  	}
    56  	if u.Scheme == "" {
    57  		return nil, errors.NotValidf("secret URI scheme missing")
    58  	}
    59  	if u.Scheme != secretScheme {
    60  		return nil, errors.NotValidf("secret URI scheme %q", u.Scheme)
    61  	}
    62  	return str, nil
    63  }
    64  
    65  // validate returns an appropriately-typed value for the supplied value, or
    66  // returns an error if it cannot be converted to the correct type. Nil values
    67  // are always considered valid.
    68  func (option Option) validate(name string, value interface{}) (_ interface{}, err error) {
    69  	if value == nil {
    70  		return nil, nil
    71  	}
    72  	if checker := optionTypeCheckers[option.Type]; checker != nil {
    73  		defer option.error(&err, name, value)
    74  		if value, err = checker.Coerce(value, nil); err != nil {
    75  			return nil, err
    76  		}
    77  		return value, nil
    78  	}
    79  	return nil, fmt.Errorf("option %q has unknown type %q", name, option.Type)
    80  }
    81  
    82  var optionTypeCheckers = map[string]schema.Checker{
    83  	"string":  schema.String(),
    84  	"int":     schema.Int(),
    85  	"float":   schema.Float(),
    86  	"boolean": schema.Bool(),
    87  	"secret":  secretC{},
    88  }
    89  
    90  func (option Option) parse(name, str string) (val interface{}, err error) {
    91  	switch option.Type {
    92  	case "string", "secret":
    93  		return str, nil
    94  	case "int":
    95  		val, err = strconv.ParseInt(str, 10, 64)
    96  	case "float":
    97  		val, err = strconv.ParseFloat(str, 64)
    98  	case "boolean":
    99  		val, err = strconv.ParseBool(str)
   100  	default:
   101  		return nil, fmt.Errorf("option %q has unknown type %q", name, option.Type)
   102  	}
   103  
   104  	defer option.error(&err, name, str)
   105  	return
   106  }
   107  
   108  // Config represents the supported configuration options for a charm,
   109  // as declared in its config.yaml file.
   110  type Config struct {
   111  	Options map[string]Option
   112  }
   113  
   114  // NewConfig returns a new Config without any options.
   115  func NewConfig() *Config {
   116  	return &Config{map[string]Option{}}
   117  }
   118  
   119  // ReadConfig reads a Config in YAML format.
   120  func ReadConfig(r io.Reader) (*Config, error) {
   121  	data, err := ioutil.ReadAll(r)
   122  	if err != nil {
   123  		return nil, err
   124  	}
   125  	var config *Config
   126  	if err := yaml.Unmarshal(data, &config); err != nil {
   127  		return nil, err
   128  	}
   129  	if config == nil {
   130  		return nil, fmt.Errorf("invalid config: empty configuration")
   131  	}
   132  	if config.Options == nil {
   133  		// We are allowed an empty configuration if the options
   134  		// field is explicitly specified, but there is no easy way
   135  		// to tell if it was specified or not without unmarshaling
   136  		// into interface{} and explicitly checking the field.
   137  		var configInterface interface{}
   138  		if err := yaml.Unmarshal(data, &configInterface); err != nil {
   139  			return nil, err
   140  		}
   141  		m, _ := configInterface.(map[interface{}]interface{})
   142  		if _, ok := m["options"]; !ok {
   143  			return nil, fmt.Errorf("invalid config: empty configuration")
   144  		}
   145  	}
   146  	for name, option := range config.Options {
   147  		switch option.Type {
   148  		case "string", "secret", "int", "float", "boolean":
   149  		case "":
   150  			// Missing type is valid in python.
   151  			option.Type = "string"
   152  		default:
   153  			return nil, fmt.Errorf("invalid config: option %q has unknown type %q", name, option.Type)
   154  		}
   155  		def := option.Default
   156  		if def == "" && (option.Type == "string" || option.Type == "secret") {
   157  			// Skip normal validation for compatibility with pyjuju.
   158  		} else if option.Default, err = option.validate(name, def); err != nil {
   159  			option.error(&err, name, def)
   160  			return nil, fmt.Errorf("invalid config default: %v", err)
   161  		}
   162  		config.Options[name] = option
   163  	}
   164  	return config, nil
   165  }
   166  
   167  // option returns the named option from the config, or an error if none
   168  // such exists.
   169  func (c *Config) option(name string) (Option, error) {
   170  	if option, ok := c.Options[name]; ok {
   171  		return option, nil
   172  	}
   173  	return Option{}, fmt.Errorf("unknown option %q", name)
   174  }
   175  
   176  // DefaultSettings returns settings containing the default value of every
   177  // option in the config. Default values may be nil.
   178  func (c *Config) DefaultSettings() Settings {
   179  	out := make(Settings)
   180  	for name, option := range c.Options {
   181  		out[name] = option.Default
   182  	}
   183  	return out
   184  }
   185  
   186  // ValidateSettings returns a copy of the supplied settings with a consistent type
   187  // for each value. It returns an error if the settings contain unknown keys
   188  // or invalid values.
   189  func (c *Config) ValidateSettings(settings Settings) (Settings, error) {
   190  	out := make(Settings)
   191  	for name, value := range settings {
   192  		if option, err := c.option(name); err != nil {
   193  			return nil, err
   194  		} else if value, err = option.validate(name, value); err != nil {
   195  			return nil, err
   196  		}
   197  		out[name] = value
   198  	}
   199  	return out, nil
   200  }
   201  
   202  // FilterSettings returns the subset of the supplied settings that are valid.
   203  func (c *Config) FilterSettings(settings Settings) Settings {
   204  	out := make(Settings)
   205  	for name, value := range settings {
   206  		if option, err := c.option(name); err == nil {
   207  			if value, err := option.validate(name, value); err == nil {
   208  				out[name] = value
   209  			}
   210  		}
   211  	}
   212  	return out
   213  }
   214  
   215  // ParseSettingsStrings returns settings derived from the supplied map. Every
   216  // value in the map must be parseable to the correct type for the option
   217  // identified by its key. Empty values are interpreted as nil.
   218  func (c *Config) ParseSettingsStrings(values map[string]string) (Settings, error) {
   219  	out := make(Settings)
   220  	for name, str := range values {
   221  		option, err := c.option(name)
   222  		if err != nil {
   223  			return nil, err
   224  		}
   225  		value, err := option.parse(name, str)
   226  		if err != nil {
   227  			return nil, err
   228  		}
   229  		out[name] = value
   230  	}
   231  	return out, nil
   232  }
   233  
   234  // ParseSettingsYAML returns settings derived from the supplied YAML data. The
   235  // YAML must unmarshal to a map of strings to settings data; the supplied key
   236  // must be present in the map, and must point to a map in which every value
   237  // must have, or be a string parseable to, the correct type for the associated
   238  // config option. Empty strings and nil values are both interpreted as nil.
   239  func (c *Config) ParseSettingsYAML(yamlData []byte, key string) (Settings, error) {
   240  	var allSettings map[string]Settings
   241  	if err := yaml.Unmarshal(yamlData, &allSettings); err != nil {
   242  		return nil, fmt.Errorf("cannot parse settings data: %v", err)
   243  	}
   244  	settings, ok := allSettings[key]
   245  	if !ok {
   246  		return nil, fmt.Errorf("no settings found for %q", key)
   247  	}
   248  	out := make(Settings)
   249  	for name, value := range settings {
   250  		option, err := c.option(name)
   251  		if err != nil {
   252  			return nil, err
   253  		}
   254  		// Accept string values for compatibility with python.
   255  		if str, ok := value.(string); ok {
   256  			if value, err = option.parse(name, str); err != nil {
   257  				return nil, err
   258  			}
   259  		} else if value, err = option.validate(name, value); err != nil {
   260  			return nil, err
   261  		}
   262  		out[name] = value
   263  	}
   264  	return out, nil
   265  }