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 }