package config import ( "bytes" "errors" "encoding/json" "fmt" "io/ioutil" "github.com/ghodss/yaml" ) // DefaultConfig contains YAML that is applied before the user's // configuration. // const DefaultConfig = `{ "lives": { "in": "/srv/app", "as": "somebody", "uid": 65533, "gid": 65533 }, "runs": { "as": "runuser", "uid": 900, "gid": 900}}` // ResolveIncludes iterates over and recurses through a given variant's // includes to build a flat slice of variant names in the correct order by // which they should be expanded/merged. It checks for both the existence of // included variants and maintains a recursion stack to protect against // infinite loops. // // Variant names found at a greater depth of recursion are first and siblings // last, the order in which config should be merged. // func ResolveIncludes(config *Config, name string) ([]string, error) { stack := map[string]bool{} includes := []string{} var resolve func(string) error resolve = func(name string) error { if instack, found := stack[name]; found && instack { return errors.New("variant expansion detected loop") } stack[name] = true defer func() { stack[name] = false }() variant, found := config.Variants[name] if !found { return fmt.Errorf("variant '%s' does not exist", name) } for _, include := range variant.Includes { if err := resolve(include); err != nil { return err } } // Appending _after_ recursion ensures the correct ordering includes = append(includes, name) return nil } err := resolve(name) return includes, err } // ExpandVariant merges a named variant with a config. It also attempts to // recursively expand any included variants in the expanded variant. // func ExpandVariant(config *Config, name string) (*VariantConfig, error) { expanded := new(VariantConfig) expanded.CommonConfig.Merge(config.CommonConfig) includes, err := ResolveIncludes(config, name) if err != nil { return nil, err } for _, include := range includes { expanded.Merge(config.Variants[include]) } return expanded, nil } // ReadYAMLConfig converts YAML bytes to json and returns new Config struct. // func ReadYAMLConfig(data []byte) (*Config, error) { jsonData, err := yaml.YAMLToJSON(data) if err != nil { return nil, err } return ReadConfig(jsonData) } // ReadConfig unmarshals the given YAML bytes into a new Config struct. // func ReadConfig(data []byte) (*Config, error) { var ( version VersionConfig config Config ) // Unmarshal config version first for pre-validation err := json.Unmarshal(data, &version) if err != nil { return nil, err } if err = Validate(version); err != nil { return nil, err } // Unmarshal the default config json.Unmarshal([]byte(DefaultConfig), &config) // And finally strictly decode the entire user-provided config dec := json.NewDecoder(bytes.NewReader(data)) dec.DisallowUnknownFields() err = dec.Decode(&config) if err != nil { return nil, err } err = Validate(config) return &config, err } // ReadConfigFile unmarshals the given YAML file contents into a Config // struct. // func ReadConfigFile(path string) (*Config, error) { data, err := ioutil.ReadFile(path) if err != nil { return nil, err } return ReadYAMLConfig(data) }