From e99289b3b767261fd0fe7df7effdda59c16f1f8d Mon Sep 17 00:00:00 2001 From: Christopher Maier Date: Fri, 22 Jul 2016 16:27:28 -0400 Subject: [PATCH] Read layered dynamic configuration from filesystem This pulls in room- and user-specific configuration for a bundle and layers it on top of the base dynamic configuration for that bundle. Only reads configuration from the filesystem; this is not hooked up to Cog's dynamic config API yet (that still only affects the base configuration), so this will only work for users that have direct filesystem access to their Relay hosts (i.e., not Hosted Cog). Fixes operable/cog#850 --- relay/config/dynamic_config.go | 81 +++++++++++++++++++++++++--------- relay/messages/env_builder.go | 2 +- 2 files changed, 62 insertions(+), 21 deletions(-) diff --git a/relay/config/dynamic_config.go b/relay/config/dynamic_config.go index c358835..336a57e 100644 --- a/relay/config/dynamic_config.go +++ b/relay/config/dynamic_config.go @@ -7,49 +7,90 @@ import ( "os" "path" "strings" + "fmt" ) // LoadDynamicConfig loads the dyanmic configuration for a bundle if // a) dynamic configuration is enabled and b) a config file exists for -// the requested bundle -func (c *Config) LoadDynamicConfig(bundle string) map[string]interface{} { +// the requested bundle. Room- and user-specific configurations are +// layered on top, if they exist. +// +// If any configuration files exist, but cannot be properly processed +// (read, parsed as YAML, etc), an empty map is returned. +func (c *Config) LoadDynamicConfig(bundle string, roomName string, userName string) map[string]interface{} { retval := make(map[string]interface{}) if c.DynamicConfigRoot == "" { log.Debugf("Dynamic config disabled.") return retval } - if fullPath := locateConfigFile(c.DynamicConfigRoot, bundle); fullPath != "" { - buf, err := ioutil.ReadFile(fullPath) + + rootInfo, rootErr := os.Stat(c.DynamicConfigRoot) + if rootErr != nil || rootInfo.IsDir() == false { + log.Debugf("Dynamic configuration root dir not found.") + return retval + } + + configs := allConfigFiles(c.DynamicConfigRoot, bundle, roomName, userName) + for _, f := range configs { + log.Debugf("Reading configuration file: %s", f) + buf, err := ioutil.ReadFile(f) if err != nil { - log.Errorf("Reading dynamic config for bundle %s failed: %s.", bundle, err) - return retval + log.Errorf("Reading dynamic config file %s failed: %s.", f, err) + return make(map[string]interface{}) } + // by unmarshalling into the same map for each layer, we + // achieve a shallow merge of the layers, with successive + // top-level keys overwriting values previously set. err = yaml.Unmarshal(buf, &retval) if err != nil { - log.Errorf("Parsing dynamic config for bundle %s failed: %s.", bundle, err) - return retval + log.Errorf("Parsing dynamic config file %s failed: %s.", f, err) + return make(map[string]interface{}) } - for k := range retval { - if strings.HasPrefix(k, "COG_") || strings.HasPrefix(k, "RELAY_") { - delete(retval, k) - log.Infof("Deleted illegal key %s from dynamic config for bundle %s.", k, bundle) - } + } + + for k := range retval { + if strings.HasPrefix(k, "COG_") || strings.HasPrefix(k, "RELAY_") { + delete(retval, k) + log.Infof("Deleted illegal key %s from dynamic config for bundle %s.", k, bundle) } } + return retval } -func locateConfigFile(configRoot string, bundle string) string { - rootInfo, rootErr := os.Stat(configRoot) - if rootErr != nil || rootInfo.IsDir() == false { - log.Debugf("Dynamic configuration root dir not found.") - return "" +// Given the config root, a bundle name, a Cog username, and a chat +// room name, return a list of all the filenames to be consolidated, +// in the order they should be layered. +func allConfigFiles(configRoot string, bundle string, room string, user string) []string { + var configs []string + + if path := resolveYAMLFile(path.Join(configRoot, bundle, "config")); path != "" { + configs = append(configs, path) + } + + roomFile := fmt.Sprintf("room_%s", strings.ToLower(room)) + if path := resolveYAMLFile(path.Join(configRoot, bundle, roomFile)); path != "" { + configs = append(configs, path) + } + + userFile := fmt.Sprintf("user_%s", strings.ToLower(user)) + if path := resolveYAMLFile(path.Join(configRoot, bundle, userFile)); path != "" { + configs = append(configs, path) } - fullYamlPath := path.Join(configRoot, bundle, "config.yaml") - fullYmlPath := path.Join(configRoot, bundle, "config.yml") + + return configs +} + +// Given a base file name, return the path to either the yaml or yml +// version, returning the ".yaml" version preferentially. +func resolveYAMLFile(base string) string { + + fullYamlPath := fmt.Sprint(base, ".yaml") + yamlInfo, yamlErr := os.Stat(fullYamlPath) if yamlErr != nil || yamlInfo.IsDir() == true { + fullYmlPath := fmt.Sprint(base, ".yml") ymlInfo, ymlErr := os.Stat(fullYmlPath) if ymlErr != nil || ymlInfo.IsDir() == true { log.Debugf("Dynamic config not found. Checked: '%s' and '%s'.", diff --git a/relay/messages/env_builder.go b/relay/messages/env_builder.go index fea32b0..e57d21f 100644 --- a/relay/messages/env_builder.go +++ b/relay/messages/env_builder.go @@ -41,7 +41,7 @@ func (er *ExecutionRequest) compileEnvironment(request *api.ExecRequest, relayCo foundDynamicConfig := false if useDynamicConfig { - dyn := relayConfig.LoadDynamicConfig(er.BundleName()) + dyn := relayConfig.LoadDynamicConfig(er.BundleName(), er.Room.Name, er.User.Username) foundDynamicConfig = len(dyn) > 0 for k, v := range dyn { request.PutEnv(k, fmt.Sprintf("%s", v))