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