diff --git a/backends/client.go b/backends/client.go index 0d554bf78..118a33b2d 100644 --- a/backends/client.go +++ b/backends/client.go @@ -34,7 +34,7 @@ func New(config Config) (StoreClient, error) { backendNodes := config.BackendNodes if config.Backend == "file" { - log.Info("Backend source(s) set to " + config.YAMLFile) + log.Info("Backend source(s) set to " + strings.Join(config.YAMLFile, ", ")) } else { log.Info("Backend source(s) set to " + strings.Join(backendNodes, ", ")) } @@ -63,7 +63,7 @@ func New(config Config) (StoreClient, error) { case "env": return env.NewEnvClient() case "file": - return file.NewFileClient(config.YAMLFile) + return file.NewFileClient(config.YAMLFile, config.Filter) case "google": return google.NewGoogleClient(backendNodes[0]) case "vault": diff --git a/backends/config.go b/backends/config.go index 6e06a83d4..8e6091a7b 100644 --- a/backends/config.go +++ b/backends/config.go @@ -1,23 +1,28 @@ package backends +import ( + util "github.com/kelseyhightower/confd/util" +) + type Config struct { - AuthToken string - AuthType string - Backend string - BasicAuth bool - ClientCaKeys string - ClientCert string - ClientKey string - BackendNodes []string - Password string - Scheme string - Table string - Separator string - Username string - AppID string - UserID string - RoleID string - SecretID string - YAMLFile string + AuthToken string `toml:"auth_token"` + AuthType string `toml:"auth_type"` + Backend string `toml:"backend"` + BasicAuth bool `toml:"basic_auth"` + ClientCaKeys string `toml:"client_cakeys"` + ClientCert string `toml:"client_cert"` + ClientKey string `toml:"client_key"` + BackendNodes util.Nodes `toml:"nodes"` + Password string `toml:"password"` + Scheme string `toml:"scheme"` + Table string `toml:"table"` + Separator string `toml:"separator"` + Username string `toml:"username"` + AppID string `toml:"app_id"` + UserID string `toml:"user_id"` + RoleID string `toml:"role_id"` + SecretID string `toml:"secret_id"` + YAMLFile util.Nodes `toml:"file"` + Filter string `toml:"filter"` Role string } diff --git a/backends/file/client.go b/backends/file/client.go index 89060d222..270dd3b81 100644 --- a/backends/file/client.go +++ b/backends/file/client.go @@ -3,10 +3,13 @@ package file import ( "fmt" "io/ioutil" + "path" + "strconv" "strings" "github.com/fsnotify/fsnotify" "github.com/kelseyhightower/confd/log" + util "github.com/kelseyhightower/confd/util" "gopkg.in/yaml.v2" ) @@ -14,56 +17,117 @@ var replacer = strings.NewReplacer("/", "_") // Client provides a shell for the yaml client type Client struct { - filepath string + filepath []string + filter string } -func NewFileClient(filepath string) (*Client, error) { - return &Client{filepath}, nil +type ResultError struct { + response uint64 + err error } -func (c *Client) GetValues(keys []string) (map[string]string, error) { - yamlMap := make(map[interface{}]interface{}) - vars := make(map[string]string) +func NewFileClient(filepath []string, filter string) (*Client, error) { + return &Client{filepath: filepath, filter: filter}, nil +} - data, err := ioutil.ReadFile(c.filepath) +func readFile(path string, vars map[string]string) error { + yamlMap := make(map[interface{}]interface{}) + data, err := ioutil.ReadFile(path) if err != nil { - return vars, err + return err } + err = yaml.Unmarshal(data, &yamlMap) if err != nil { - return vars, err + return err } - nodeWalk(yamlMap, "", vars) - log.Debug(fmt.Sprintf("Key Map: %#v", vars)) + err = nodeWalk(yamlMap, "/", vars) + if err != nil { + return err + } + return nil +} +func (c *Client) GetValues(keys []string) (map[string]string, error) { + vars := make(map[string]string) + var filePaths []string + for _, path := range c.filepath { + p, err := util.RecursiveFilesLookup(path, c.filter) + if err != nil { + return nil, err + } + filePaths = append(filePaths, p...) + } + + for _, path := range filePaths { + err := readFile(path, vars) + if err != nil { + return nil, err + } + } + +VarsLoop: + for k, _ := range vars { + for _, key := range keys { + if strings.HasPrefix(k, key) { + continue VarsLoop + } + } + delete(vars, k) + } + log.Debug(fmt.Sprintf("Key Map: %#v", vars)) return vars, nil } // nodeWalk recursively descends nodes, updating vars. -func nodeWalk(node map[interface{}]interface{}, key string, vars map[string]string) error { - for k, v := range node { - key := key + "/" + k.(string) - - switch v.(type) { - case map[interface{}]interface{}: - nodeWalk(v.(map[interface{}]interface{}), key, vars) - case []interface{}: - for _, j := range v.([]interface{}) { - switch j.(type) { - case map[interface{}]interface{}: - nodeWalk(j.(map[interface{}]interface{}), key, vars) - case string: - vars[key+"/"+j.(string)] = "" - } - } - case string: - vars[key] = v.(string) +func nodeWalk(node interface{}, key string, vars map[string]string) error { + switch node.(type) { + case []interface{}: + for i, j := range node.([]interface{}) { + key := path.Join(key, strconv.Itoa(i)) + nodeWalk(j, key, vars) } + case map[interface{}]interface{}: + for k, v := range node.(map[interface{}]interface{}) { + key := path.Join(key, k.(string)) + nodeWalk(v, key, vars) + } + case string: + vars[key] = node.(string) + case int: + vars[key] = strconv.Itoa(node.(int)) + case bool: + vars[key] = strconv.FormatBool(node.(bool)) + case float64: + vars[key] = strconv.FormatFloat(node.(float64), 'f', -1, 64) } return nil } +func (c *Client) watchChanges(watcher *fsnotify.Watcher, stopChan chan bool) ResultError { + outputChannel := make(chan ResultError) + defer close(outputChannel) + go func() error { + for { + select { + case event := <-watcher.Events: + log.Debug("event:", event) + if event.Op&fsnotify.Write == fsnotify.Write || + event.Op&fsnotify.Remove == fsnotify.Remove || + event.Op&fsnotify.Create == fsnotify.Create { + outputChannel <- ResultError{response: 1, err: nil} + } + case err := <-watcher.Errors: + outputChannel <- ResultError{response: 0, err: err} + case <-stopChan: + outputChannel <- ResultError{response: 1, err: nil} + } + } + }() + return <-outputChannel +} + func (c *Client) WatchPrefix(prefix string, keys []string, waitIndex uint64, stopChan chan bool) (uint64, error) { if waitIndex == 0 { return 1, nil @@ -74,23 +138,32 @@ func (c *Client) WatchPrefix(prefix string, keys []string, waitIndex uint64, sto return 0, err } defer watcher.Close() - - err = watcher.Add(c.filepath) - if err != nil { - return 0, err - } - - for { - select { - case event := <-watcher.Events: - if event.Op&fsnotify.Write == fsnotify.Write || event.Op&fsnotify.Remove == fsnotify.Remove { - return 1, nil - } - case err := <-watcher.Errors: + for _, path := range c.filepath { + isDir, err := util.IsDirectory(path) + if err != nil { return 0, err - case <-stopChan: - return 0, nil } + if isDir { + dirs, err := util.RecursiveDirsLookup(path, "*") + if err != nil { + return 0, err + } + for _, dir := range dirs { + err = watcher.Add(dir) + if err != nil { + return 0, err + } + } + } else { + err = watcher.Add(path) + if err != nil { + return 0, err + } + } + } + output := c.watchChanges(watcher, stopChan) + if output.response != 2 { + return output.response, output.err } return waitIndex, nil } diff --git a/confd.go b/confd.go index 8bf323bee..e0ccbf1af 100644 --- a/confd.go +++ b/confd.go @@ -15,7 +15,7 @@ import ( func main() { flag.Parse() - if printVersion { + if config.PrintVersion { fmt.Printf("confd %s (Git SHA: %s, Go Version: %s)\n", Version, GitSHA, runtime.Version()) os.Exit(0) } @@ -25,14 +25,14 @@ func main() { log.Info("Starting confd") - storeClient, err := backends.New(backendsConfig) + storeClient, err := backends.New(config.BackendsConfig) if err != nil { log.Fatal(err.Error()) } - templateConfig.StoreClient = storeClient - if onetime { - if err := template.Process(templateConfig); err != nil { + config.TemplateConfig.StoreClient = storeClient + if config.OneTime { + if err := template.Process(config.TemplateConfig); err != nil { log.Fatal(err.Error()) } os.Exit(0) @@ -45,9 +45,9 @@ func main() { var processor template.Processor switch { case config.Watch: - processor = template.WatchProcessor(templateConfig, stopChan, doneChan, errChan) + processor = template.WatchProcessor(config.TemplateConfig, stopChan, doneChan, errChan) default: - processor = template.IntervalProcessor(templateConfig, stopChan, doneChan, errChan, config.Interval) + processor = template.IntervalProcessor(config.TemplateConfig, stopChan, doneChan, errChan, config.Interval) } go processor.Process() diff --git a/config.go b/config.go index 108213c88..162224d08 100644 --- a/config.go +++ b/config.go @@ -17,110 +17,60 @@ import ( "github.com/kelseyhightower/confd/resource/template" ) -var ( - configFile = "" - defaultConfigFile = "/etc/confd/confd.toml" - authToken string - authType string - backend string - basicAuth bool - clientCaKeys string - clientCert string - clientKey string - confdir string - config Config // holds the global confd config. - interval int - keepStageFile bool - logLevel string - nodes Nodes - noop bool - onetime bool - prefix string - printVersion bool - secretKeyring string - scheme string - srvDomain string - srvRecord string - syncOnly bool - table string - separator string - templateConfig template.Config - backendsConfig backends.Config - username string - password string - watch bool - appID string - userID string - roleID string - secretID string - yamlFile string -) +type TemplateConfig = template.Config +type BackendsConfig = backends.Config // A Config structure is used to configure confd. type Config struct { - AuthToken string `toml:"auth_token"` - AuthType string `toml:"auth_type"` - Backend string `toml:"backend"` - BasicAuth bool `toml:"basic_auth"` - BackendNodes []string `toml:"nodes"` - ClientCaKeys string `toml:"client_cakeys"` - ClientCert string `toml:"client_cert"` - ClientKey string `toml:"client_key"` - ConfDir string `toml:"confdir"` - Interval int `toml:"interval"` - SecretKeyring string `toml:"secret_keyring"` - Noop bool `toml:"noop"` - Password string `toml:"password"` - Prefix string `toml:"prefix"` - SRVDomain string `toml:"srv_domain"` - SRVRecord string `toml:"srv_record"` - Scheme string `toml:"scheme"` - SyncOnly bool `toml:"sync-only"` - Table string `toml:"table"` - Separator string `toml:"separator"` - Username string `toml:"username"` - LogLevel string `toml:"log-level"` - Watch bool `toml:"watch"` - AppID string `toml:"app_id"` - UserID string `toml:"user_id"` - RoleID string `toml:"role_id"` - SecretID string `toml:"secret_id"` - YAMLFile string `toml:"file"` + TemplateConfig + BackendsConfig + Interval int `toml:"interval"` + SecretKeyring string `toml:"secret_keyring"` + SRVDomain string `toml:"srv_domain"` + SRVRecord string `toml:"srv_record"` + LogLevel string `toml:"log-level"` + Watch bool `toml:"watch"` + PrintVersion bool + ConfigFile string + OneTime bool } +var config Config + func init() { - flag.StringVar(&authToken, "auth-token", "", "Auth bearer token to use") - flag.StringVar(&backend, "backend", "etcd", "backend to use") - flag.BoolVar(&basicAuth, "basic-auth", false, "Use Basic Auth to authenticate (only used with -backend=consul and -backend=etcd)") - flag.StringVar(&clientCaKeys, "client-ca-keys", "", "client ca keys") - flag.StringVar(&clientCert, "client-cert", "", "the client cert") - flag.StringVar(&clientKey, "client-key", "", "the client key") - flag.StringVar(&confdir, "confdir", "/etc/confd", "confd conf directory") - flag.StringVar(&configFile, "config-file", "", "the confd config file") - flag.StringVar(&yamlFile, "file", "", "the YAML/JSON file to watch for changes") - flag.IntVar(&interval, "interval", 600, "backend polling interval") - flag.BoolVar(&keepStageFile, "keep-stage-file", false, "keep staged files") - flag.StringVar(&logLevel, "log-level", "", "level which confd should log messages") - flag.Var(&nodes, "node", "list of backend nodes") - flag.BoolVar(&noop, "noop", false, "only show pending changes") - flag.BoolVar(&onetime, "onetime", false, "run once and exit") - flag.StringVar(&prefix, "prefix", "", "key path prefix") - flag.BoolVar(&printVersion, "version", false, "print version and exit") - flag.StringVar(&scheme, "scheme", "http", "the backend URI scheme for nodes retrieved from DNS SRV records (http or https)") - flag.StringVar(&secretKeyring, "secret-keyring", "", "path to armored PGP secret keyring (for use with crypt functions)") - flag.StringVar(&srvDomain, "srv-domain", "", "the name of the resource record") - flag.StringVar(&srvRecord, "srv-record", "", "the SRV record to search for backends nodes. Example: _etcd-client._tcp.example.com") - flag.BoolVar(&syncOnly, "sync-only", false, "sync without check_cmd and reload_cmd") - flag.StringVar(&authType, "auth-type", "", "Vault auth backend type to use (only used with -backend=vault)") - flag.StringVar(&appID, "app-id", "", "Vault app-id to use with the app-id backend (only used with -backend=vault and auth-type=app-id)") - flag.StringVar(&userID, "user-id", "", "Vault user-id to use with the app-id backend (only used with -backend=value and auth-type=app-id)") - flag.StringVar(&roleID, "role-id", "", "Vault role-id to use with the AppRole, Kubernetes backends (only used with -backend=vault and either auth-type=app-role or auth-type=kubernetes)") - flag.StringVar(&secretID, "secret-id", "", "Vault secret-id to use with the AppRole backend (only used with -backend=vault and auth-type=app-role)") - flag.StringVar(&table, "table", "", "the name of the DynamoDB table (only used with -backend=dynamodb)") - flag.StringVar(&separator, "separator", "", "the separator to replace '/' with when looking up keys in the backend, prefixed '/' will also be removed (only used with -backend=redis)") - flag.StringVar(&username, "username", "", "the username to authenticate as (only used with vault and etcd backends)") - flag.StringVar(&password, "password", "", "the password to authenticate with (only used with vault and etcd backends)") - flag.BoolVar(&watch, "watch", false, "enable watch support") + flag.StringVar(&config.AuthToken, "auth-token", "", "Auth bearer token to use") + flag.StringVar(&config.Backend, "backend", "etcd", "backend to use") + flag.BoolVar(&config.BasicAuth, "basic-auth", false, "Use Basic Auth to authenticate (only used with -backend=consul and -backend=etcd)") + flag.StringVar(&config.ClientCaKeys, "client-ca-keys", "", "client ca keys") + flag.StringVar(&config.ClientCert, "client-cert", "", "the client cert") + flag.StringVar(&config.ClientKey, "client-key", "", "the client key") + flag.StringVar(&config.ConfDir, "confdir", "/etc/confd", "confd conf directory") + flag.StringVar(&config.ConfigFile, "config-file", "/etc/confd/confd.toml", "the confd config file") + flag.Var(&config.YAMLFile, "file", "the YAML file to watch for changes (only used with -backend=file)") + flag.StringVar(&config.Filter, "filter", "*", "files filter (only used with -backend=file)") + flag.IntVar(&config.Interval, "interval", 600, "backend polling interval") + flag.BoolVar(&config.KeepStageFile, "keep-stage-file", false, "keep staged files") + flag.StringVar(&config.LogLevel, "log-level", "", "level which confd should log messages") + flag.Var(&config.BackendNodes, "node", "list of backend nodes") + flag.BoolVar(&config.Noop, "noop", false, "only show pending changes") + flag.BoolVar(&config.OneTime, "onetime", false, "run once and exit") + flag.StringVar(&config.Prefix, "prefix", "", "key path prefix") + flag.BoolVar(&config.PrintVersion, "version", false, "print version and exit") + flag.StringVar(&config.Scheme, "scheme", "http", "the backend URI scheme for nodes retrieved from DNS SRV records (http or https)") + flag.StringVar(&config.SecretKeyring, "secret-keyring", "", "path to armored PGP secret keyring (for use with crypt functions)") + flag.StringVar(&config.SRVDomain, "srv-domain", "", "the name of the resource record") + flag.StringVar(&config.SRVRecord, "srv-record", "", "the SRV record to search for backends nodes. Example: _etcd-client._tcp.example.com") + flag.BoolVar(&config.SyncOnly, "sync-only", false, "sync without check_cmd and reload_cmd") + flag.StringVar(&config.AuthType, "auth-type", "", "Vault auth backend type to use (only used with -backend=vault)") + flag.StringVar(&config.AppID, "app-id", "", "Vault app-id to use with the app-id backend (only used with -backend=vault and auth-type=app-id)") + flag.StringVar(&config.UserID, "user-id", "", "Vault user-id to use with the app-id backend (only used with -backend=value and auth-type=app-id)") + flag.StringVar(&config.RoleID, "role-id", "", "Vault role-id to use with the AppRole, Kubernetes backends (only used with -backend=vault and either auth-type=app-role or auth-type=kubernetes)") + flag.StringVar(&config.SecretID, "secret-id", "", "Vault secret-id to use with the AppRole backend (only used with -backend=vault and auth-type=app-role)") + flag.StringVar(&config.Table, "table", "", "the name of the DynamoDB table (only used with -backend=dynamodb)") + flag.StringVar(&config.Separator, "separator", "", "the separator to replace '/' with when looking up keys in the backend, prefixed '/' will also be removed (only used with -backend=redis)") + flag.StringVar(&config.Username, "username", "", "the username to authenticate as (only used with vault and etcd backends)") + flag.StringVar(&config.Password, "password", "", "the password to authenticate with (only used with vault and etcd backends)") + flag.BoolVar(&config.Watch, "watch", false, "enable watch support") } // initConfig initializes the confd configuration by first setting defaults, @@ -129,28 +79,16 @@ func init() { // settings from flags set on the command line. // It returns an error if any. func initConfig() error { - if configFile == "" { - if _, err := os.Stat(defaultConfigFile); !os.IsNotExist(err) { - configFile = defaultConfigFile - } - } - // Set defaults. - config = Config{ - Backend: "etcd", - ConfDir: "/etc/confd", - Interval: 600, - Prefix: "", - Scheme: "http", - } - // Update config from the TOML configuration file. - if configFile == "" { + _, err := os.Stat(config.ConfigFile) + if os.IsNotExist(err) { log.Debug("Skipping confd config file.") } else { - log.Debug("Loading " + configFile) - configBytes, err := ioutil.ReadFile(configFile) + log.Debug("Loading " + config.ConfigFile) + configBytes, err := ioutil.ReadFile(config.ConfigFile) if err != nil { return err } + _, err = toml.Decode(string(configBytes), &config) if err != nil { return err @@ -160,16 +98,13 @@ func initConfig() error { // Update config from environment variables. processEnv() - // Update config from commandline flags. - processFlags() - var pgpPrivateKey []byte if config.SecretKeyring != "" { kr, err := os.Open(config.SecretKeyring) if err != nil { log.Fatal(err.Error()) } defer kr.Close() - pgpPrivateKey, err = ioutil.ReadAll(kr) + config.PGPPrivateKey, err = ioutil.ReadAll(kr) if err != nil { log.Fatal(err.Error()) } @@ -243,38 +178,8 @@ func initConfig() error { if config.Backend == "dynamodb" && config.Table == "" { return errors.New("No DynamoDB table configured") } - - backendsConfig = backends.Config{ - AuthToken: config.AuthToken, - AuthType: config.AuthType, - Backend: config.Backend, - BasicAuth: config.BasicAuth, - ClientCaKeys: config.ClientCaKeys, - ClientCert: config.ClientCert, - ClientKey: config.ClientKey, - BackendNodes: config.BackendNodes, - Password: config.Password, - Scheme: config.Scheme, - Table: config.Table, - Separator: config.Separator, - Username: config.Username, - AppID: config.AppID, - UserID: config.UserID, - RoleID: config.RoleID, - SecretID: config.SecretID, - YAMLFile: config.YAMLFile, - } - // Template configuration. - templateConfig = template.Config{ - ConfDir: config.ConfDir, - ConfigDir: filepath.Join(config.ConfDir, "conf.d"), - KeepStageFile: keepStageFile, - Noop: config.Noop, - Prefix: config.Prefix, - SyncOnly: config.SyncOnly, - TemplateDir: filepath.Join(config.ConfDir, "templates"), - PGPPrivateKey: pgpPrivateKey, - } + config.ConfigDir = filepath.Join(config.ConfDir, "conf.d") + config.TemplateDir = filepath.Join(config.ConfDir, "templates") return nil } @@ -294,86 +199,19 @@ func getBackendNodesFromSRV(record string) ([]string, error) { return nodes, nil } -// processFlags iterates through each flag set on the command line and -// overrides corresponding configuration settings. -func processFlags() { - flag.Visit(setConfigFromFlag) -} - func processEnv() { cakeys := os.Getenv("CONFD_CLIENT_CAKEYS") - if len(cakeys) > 0 { + if len(cakeys) > 0 && config.ClientCaKeys == "" { config.ClientCaKeys = cakeys } cert := os.Getenv("CONFD_CLIENT_CERT") - if len(cert) > 0 { + if len(cert) > 0 && config.ClientCert == "" { config.ClientCert = cert } key := os.Getenv("CONFD_CLIENT_KEY") - if len(key) > 0 { + if len(key) > 0 && config.ClientKey == "" { config.ClientKey = key } } - -func setConfigFromFlag(f *flag.Flag) { - switch f.Name { - case "auth-token": - config.AuthToken = authToken - case "auth-type": - config.AuthType = authType - case "backend": - config.Backend = backend - case "basic-auth": - config.BasicAuth = basicAuth - case "client-cert": - config.ClientCert = clientCert - case "client-key": - config.ClientKey = clientKey - case "client-ca-keys": - config.ClientCaKeys = clientCaKeys - case "confdir": - config.ConfDir = confdir - case "node": - config.BackendNodes = nodes - case "interval": - config.Interval = interval - case "noop": - config.Noop = noop - case "password": - config.Password = password - case "prefix": - config.Prefix = prefix - case "scheme": - config.Scheme = scheme - case "secret-keyring": - config.SecretKeyring = secretKeyring - case "srv-domain": - config.SRVDomain = srvDomain - case "srv-record": - config.SRVRecord = srvRecord - case "sync-only": - config.SyncOnly = syncOnly - case "table": - config.Table = table - case "separator": - config.Separator = separator - case "username": - config.Username = username - case "log-level": - config.LogLevel = logLevel - case "watch": - config.Watch = watch - case "app-id": - config.AppID = appID - case "user-id": - config.UserID = userID - case "role-id": - config.RoleID = roleID - case "secret-id": - config.SecretID = secretID - case "file": - config.YAMLFile = yamlFile - } -} diff --git a/config_test.go b/config_test.go index 565f201f6..541684343 100644 --- a/config_test.go +++ b/config_test.go @@ -10,19 +10,20 @@ import ( func TestInitConfigDefaultConfig(t *testing.T) { log.SetLevel("warn") want := Config{ - Backend: "etcd", - BackendNodes: []string{"http://127.0.0.1:4001"}, - ClientCaKeys: "", - ClientCert: "", - ClientKey: "", - ConfDir: "/etc/confd", - Interval: 600, - Noop: false, - Prefix: "", - SRVDomain: "", - Scheme: "http", - SecretKeyring: "", - Table: "", + BackendsConfig: BackendsConfig{ + Backend: "etcd", + BackendNodes: []string{"http://127.0.0.1:4001"}, + Scheme: "http", + Filter: "*", + }, + TemplateConfig: TemplateConfig{ + ConfDir: "/etc/confd", + ConfigDir: "/etc/confd/conf.d", + TemplateDir: "/etc/confd/templates", + Noop: false, + }, + ConfigFile: "/etc/confd/confd.toml", + Interval: 600, } if err := initConfig(); err != nil { t.Errorf(err.Error()) diff --git a/docs/command-line-flags.md b/docs/command-line-flags.md index 797711df4..902d1b40c 100644 --- a/docs/command-line-flags.md +++ b/docs/command-line-flags.md @@ -29,7 +29,9 @@ Usage of confd: -config-file string the confd config file -file string - the YAML/JSON file to watch for changes + list of files/directories with data represented in YAML to watch for changes + -filter string + regex for files and dirs filtering -interval int backend polling interval (default 600) -keep-stage-file diff --git a/docs/vault-kubernetes-auth.md b/docs/vault-kubernetes-auth.md index 91942297f..ef6030405 100644 --- a/docs/vault-kubernetes-auth.md +++ b/docs/vault-kubernetes-auth.md @@ -4,8 +4,8 @@ These are steps to get vault with Kubernetes auth working on minikube. - Deploy Helm ``` - # Install Helm - on macOS - brew install kubernetes-helm + # Install Helm + Use the correct method for your OS from https://docs.helm.sh/using_helm/#installing-the-helm-client # Deploy tiller into the cluster helm init @@ -14,8 +14,8 @@ These are steps to get vault with Kubernetes auth working on minikube. # Add Vault chart helm repo add incubator http://storage.googleapis.com/kubernetes-charts-incubator # Install Vault - # Currently the chart has Vault 0.8.2 and we need 0.8.3 (but PR is pending) - helm install incubator/vault --name vault --set vault.dev=true --set image.tag="0.8.3" + # We need at least Vault 0.8.3 + helm install incubator/vault --name vault --set vault.dev=true --set image.tag="0.9.5" ``` - Enable Kubernetes backend @@ -26,16 +26,26 @@ These are steps to get vault with Kubernetes auth working on minikube. kubectl exec -i -t ${POD_NAME} sh # Set env vars for Vault client export VAULT_TOKEN=$(cat /root/.vault-token) + # Set Vault host URL (do this everytime you exec back into container) + export VAULT_ADDR=http://127.0.0.1:8200 # Enable Kube auth backend - vault auth-enable kubernetes - # Configure Kube auth bacckend + vault auth enable kubernetes + # Configure Kube auth backend vault write auth/kubernetes/config \ kubernetes_host=https://kubernetes \ kubernetes_ca_cert=@/var/run/secrets/kubernetes.io/serviceaccount/ca.crt # Create Vault policy for testing - vault write sys/policy/test \ - rules='path "secret/*" { capabilities = ["create", "read"] }' - # Cretate role for confd + vault policy write test -< /etc/confd/templates/test.conf.tmpl # and finally run confd - confd -onetime -backend vault -auth-type kubernetes -role-id confd -node http://unrealistic-sabertooth-vault:8200 -log-level debug + confd -onetime -backend vault -auth-type kubernetes -role confd -node http://vault-vault:8200 -log-level debug ``` - Check `/tmp/test.conf`, it should contain your secret ``` cat /tmp/test.conf - ``` \ No newline at end of file + ``` diff --git a/integration/file/test.sh b/integration/file/test.sh index 0ab40e5a6..2467eb0f1 100644 --- a/integration/file/test.sh +++ b/integration/file/test.sh @@ -1,27 +1,39 @@ #!/bin/bash export HOSTNAME="localhost" - -cat <> test.yaml +mkdir backends1 backends2 +cat <> backends1/1.yaml key: foobar database: - - host: 127.0.0.1 - - password: p@sSw0rd - - port: "3306" - - username: confd + host: 127.0.0.1 + password: p@sSw0rd + port: "3306" + username: confd +EOT + +cat <> backends1/2.yaml upstream: - - app1: 10.0.1.10:8080 - - app2: 10.0.1.11:8080 + app1: 10.0.1.10:8080 + app2: 10.0.1.11:8080 +EOT + +cat <> backends2/1.yaml +nested: + app1: 10.0.1.10:8080 + app2: 10.0.1.11:8080 +EOT + +cat <> backends2/2.yaml prefix: database: - - host: 127.0.0.1 - - password: p@sSw0rd - - port: "3306" - - username: confd + host: 127.0.0.1 + password: p@sSw0rd + port: "3306" + username: confd upstream: app1: 10.0.1.10:8080 app2: 10.0.1.11:8080 EOT # Run confd -confd --onetime --log-level debug --confdir ./integration/confdir --backend file --file test.yaml --watch +confd --onetime --log-level debug --confdir ./integration/confdir --backend file --file backends1/ --file backends2/ --watch diff --git a/node_var.go b/node_var.go deleted file mode 100644 index 74edfc820..000000000 --- a/node_var.go +++ /dev/null @@ -1,19 +0,0 @@ -package main - -import ( - "fmt" -) - -// Nodes is a custom flag Var representing a list of etcd nodes. -type Nodes []string - -// String returns the string representation of a node var. -func (n *Nodes) String() string { - return fmt.Sprintf("%s", *n) -} - -// Set appends the node to the etcd node list. -func (n *Nodes) Set(node string) error { - *n = append(*n, node) - return nil -} diff --git a/resource/template/processor.go b/resource/template/processor.go index 4fee06b0e..3e6aec901 100644 --- a/resource/template/processor.go +++ b/resource/template/processor.go @@ -6,6 +6,7 @@ import ( "time" "github.com/kelseyhightower/confd/log" + util "github.com/kelseyhightower/confd/util" ) type Processor interface { @@ -91,7 +92,7 @@ func (p *watchProcessor) Process() { func (p *watchProcessor) monitorPrefix(t *TemplateResource) { defer p.wg.Done() - keys := appendPrefix(t.Prefix, t.Keys) + keys := util.AppendPrefix(t.Prefix, t.Keys) for { index, err := t.storeClient.WatchPrefix(t.Prefix, keys, t.lastIndex, p.stopChan) if err != nil { @@ -111,11 +112,11 @@ func getTemplateResources(config Config) ([]*TemplateResource, error) { var lastError error templates := make([]*TemplateResource, 0) log.Debug("Loading template resources from confdir " + config.ConfDir) - if !isFileExist(config.ConfDir) { + if !util.IsFileExist(config.ConfDir) { log.Warning(fmt.Sprintf("Cannot load template resources: confdir '%s' does not exist", config.ConfDir)) return nil, nil } - paths, err := recursiveFindFiles(config.ConfigDir, "*toml") + paths, err := util.RecursiveFilesLookup(config.ConfigDir, "*toml") if err != nil { return nil, err } diff --git a/resource/template/resource.go b/resource/template/resource.go index 0147ed0ed..f5da2a06a 100644 --- a/resource/template/resource.go +++ b/resource/template/resource.go @@ -17,18 +17,19 @@ import ( "github.com/BurntSushi/toml" "github.com/kelseyhightower/confd/backends" "github.com/kelseyhightower/confd/log" + util "github.com/kelseyhightower/confd/util" "github.com/kelseyhightower/memkv" "github.com/xordataexchange/crypt/encoding/secconf" ) type Config struct { - ConfDir string + ConfDir string `toml:"confdir"` ConfigDir string KeepStageFile bool - Noop bool - Prefix string + Noop bool `toml:"noop"` + Prefix string `toml:"prefix"` StoreClient backends.StoreClient - SyncOnly bool + SyncOnly bool `toml:"sync-only"` TemplateDir string PGPPrivateKey []byte } @@ -176,7 +177,7 @@ func (t *TemplateResource) setVars() error { log.Debug("Retrieving keys from store") log.Debug("Key prefix set to " + t.Prefix) - result, err := t.storeClient.GetValues(appendPrefix(t.Prefix, t.Keys)) + result, err := t.storeClient.GetValues(util.AppendPrefix(t.Prefix, t.Keys)) if err != nil { return err } @@ -197,7 +198,7 @@ func (t *TemplateResource) setVars() error { func (t *TemplateResource) createStageFile() error { log.Debug("Using source template " + t.Src) - if !isFileExist(t.Src) { + if !util.IsFileExist(t.Src) { return errors.New("Missing template: " + t.Src) } @@ -243,7 +244,7 @@ func (t *TemplateResource) sync() error { } log.Debug("Comparing candidate config to " + t.Dest) - ok, err := sameConfig(staged, t.Dest) + ok, err := util.IsConfigChanged(staged, t.Dest) if err != nil { log.Error(err.Error()) } @@ -251,7 +252,7 @@ func (t *TemplateResource) sync() error { log.Warning("Noop mode enabled. " + t.Dest + " will not be modified") return nil } - if !ok { + if ok { log.Info("Target config " + t.Dest + " out of sync") if !t.syncOnly && t.CheckCmd != "" { if err := t.check(); err != nil { @@ -364,7 +365,7 @@ func (t *TemplateResource) process() error { // setFileMode sets the FileMode. func (t *TemplateResource) setFileMode() error { if t.Mode == "" { - if !isFileExist(t.Dest) { + if !util.IsFileExist(t.Dest) { t.FileMode = 0644 } else { fi, err := os.Stat(t.Dest) diff --git a/resource/template/resource_test.go b/resource/template/resource_test.go index 10f87c267..f7d9e7f4b 100644 --- a/resource/template/resource_test.go +++ b/resource/template/resource_test.go @@ -107,61 +107,3 @@ func TestProcessTemplateResources(t *testing.T) { t.Errorf("Expected contents of dest == '%s', got %s", expected, string(results)) } } - -func TestSameConfigTrue(t *testing.T) { - log.SetLevel("warn") - src, err := ioutil.TempFile("", "src") - defer os.Remove(src.Name()) - if err != nil { - t.Errorf(err.Error()) - } - _, err = src.WriteString("foo") - if err != nil { - t.Errorf(err.Error()) - } - dest, err := ioutil.TempFile("", "dest") - defer os.Remove(dest.Name()) - if err != nil { - t.Errorf(err.Error()) - } - _, err = dest.WriteString("foo") - if err != nil { - t.Errorf(err.Error()) - } - status, err := sameConfig(src.Name(), dest.Name()) - if err != nil { - t.Errorf(err.Error()) - } - if status != true { - t.Errorf("Expected sameConfig(src, dest) to be %v, got %v", true, status) - } -} - -func TestSameConfigFalse(t *testing.T) { - log.SetLevel("warn") - src, err := ioutil.TempFile("", "src") - defer os.Remove(src.Name()) - if err != nil { - t.Errorf(err.Error()) - } - _, err = src.WriteString("src") - if err != nil { - t.Errorf(err.Error()) - } - dest, err := ioutil.TempFile("", "dest") - defer os.Remove(dest.Name()) - if err != nil { - t.Errorf(err.Error()) - } - _, err = dest.WriteString("dest") - if err != nil { - t.Errorf(err.Error()) - } - status, err := sameConfig(src.Name(), dest.Name()) - if err != nil { - t.Errorf(err.Error()) - } - if status != false { - t.Errorf("Expected sameConfig(src, dest) to be %v, got %v", false, status) - } -} diff --git a/resource/template/template_funcs.go b/resource/template/template_funcs.go index 096a17516..9a5d94f29 100644 --- a/resource/template/template_funcs.go +++ b/resource/template/template_funcs.go @@ -13,6 +13,7 @@ import ( "strings" "time" + util "github.com/kelseyhightower/confd/util" "github.com/kelseyhightower/memkv" ) @@ -33,8 +34,10 @@ func newFuncMap() map[string]interface{} { m["replace"] = strings.Replace m["trimSuffix"] = strings.TrimSuffix m["lookupIP"] = LookupIP + m["lookupIPV4"] = LookupIPV4 + m["lookupIPV6"] = LookupIPV6 m["lookupSRV"] = LookupSRV - m["fileExists"] = isFileExist + m["fileExists"] = util.IsFileExist m["base64Encode"] = Base64Encode m["base64Decode"] = Base64Decode m["parseBool"] = strconv.ParseBool @@ -181,6 +184,26 @@ func LookupIP(data string) []string { return ipStrings } +func LookupIPV6(data string) []string { + var addresses []string + for _, ip := range LookupIP(data) { + if strings.Contains(ip, ":") { + addresses = append(addresses, ip) + } + } + return addresses +} + +func LookupIPV4(data string) []string { + var addresses []string + for _, ip := range LookupIP(data) { + if strings.Contains(ip, ".") { + addresses = append(addresses, ip) + } + } + return addresses +} + type sortSRV []*net.SRV func (s sortSRV) Len() int { diff --git a/resource/template/template_test.go b/resource/template/template_test.go index 4828654a5..adafbea27 100644 --- a/resource/template/template_test.go +++ b/resource/template/template_test.go @@ -109,7 +109,7 @@ type templateTest struct { desc string // description of the test (for helpful errors) toml string // toml file contents tmpl string // template file contents - expected string // expected generated file contents + expected interface{} // expected generated file contents updateStore func(*TemplateResource) // function for setting values in store } @@ -615,6 +615,62 @@ dir: /test/data tr.store.Set("/test/data/def", "child") }, }, + templateTest{ + desc: "ipv4 lookup test", + toml: ` +[template] +src = "test.conf.tmpl" +dest = "./tmp/test.conf" +keys = [ + "/test/data", + "/test/data/abc", +] +`, + tmpl: ` +{{range lookupIPV4 "localhost"}} +ip: {{.}} +{{end}} +`, + expected: ` + +ip: 127.0.0.1 + +`, + updateStore: func(tr *TemplateResource) { + tr.store.Set("/test/data", "parent") + tr.store.Set("/test/data/def", "child") + }, + }, + templateTest{ + desc: "ipv6 lookup test", + toml: ` +[template] +src = "test.conf.tmpl" +dest = "./tmp/test.conf" +keys = [ + "/test/data", + "/test/data/abc", +] +`, + tmpl: ` +{{range lookupIPV6 "localhost"}} +ip: {{.}} +{{end}} +`, + expected: [...]string{ + ` +ip: ::1 + +`, + ` + +`, + }, + updateStore: func(tr *TemplateResource) { + tr.store.Set("/test/data", "parent") + tr.store.Set("/test/data/def", "child") + }, + }, templateTest{ desc: "ip lookup test", toml: ` @@ -631,11 +687,25 @@ keys = [ ip: {{.}} {{end}} `, - expected: ` + expected: [...]string{ + ` + +ip: 127.0.0.1 + +`, + ` ip: 127.0.0.1 +ip: ::1 + `, + ` + +ip: ::1 + +`, + }, updateStore: func(tr *TemplateResource) { tr.store.Set("/test/data", "parent") tr.store.Set("/test/data/def", "child") @@ -749,8 +819,18 @@ func ExecuteTestTemplate(tt templateTest, t *testing.T) { if err != nil { t.Errorf(tt.desc + ": failed to read StageFile: " + err.Error()) } - if string(actual) != tt.expected { - t.Errorf(fmt.Sprintf("%v: invalid StageFile. Expected %v, actual %v", tt.desc, tt.expected, string(actual))) + switch tt.expected.(type) { + case string: + if string(actual) != tt.expected.(string) { + t.Errorf(fmt.Sprintf("%v: invalid StageFile. Expected %v, actual %v", tt.desc, tt.expected, string(actual))) + } + case []string: + for _, expected := range tt.expected.([]string) { + if string(actual) == expected { + break + } + } + t.Errorf(fmt.Sprintf("%v: invalid StageFile. Possible expected values %v, actual %v", tt.desc, tt.expected, string(actual))) } } diff --git a/resource/template/util.go b/resource/template/util.go deleted file mode 100644 index cab6dc276..000000000 --- a/resource/template/util.go +++ /dev/null @@ -1,90 +0,0 @@ -package template - -import ( - "fmt" - "os" - "path" - "path/filepath" - - "github.com/kelseyhightower/confd/log" -) - -// fileInfo describes a configuration file and is returned by fileStat. -type fileInfo struct { - Uid uint32 - Gid uint32 - Mode os.FileMode - Md5 string -} - -func appendPrefix(prefix string, keys []string) []string { - s := make([]string, len(keys)) - for i, k := range keys { - s[i] = path.Join(prefix, k) - } - return s -} - -// isFileExist reports whether path exits. -func isFileExist(fpath string) bool { - if _, err := os.Stat(fpath); os.IsNotExist(err) { - return false - } - return true -} - -// sameConfig reports whether src and dest config files are equal. -// Two config files are equal when they have the same file contents and -// Unix permissions. The owner, group, and mode must match. -// It return false in other cases. -func sameConfig(src, dest string) (bool, error) { - if !isFileExist(dest) { - return false, nil - } - d, err := fileStat(dest) - if err != nil { - return false, err - } - s, err := fileStat(src) - if err != nil { - return false, err - } - if d.Uid != s.Uid { - log.Info(fmt.Sprintf("%s has UID %d should be %d", dest, d.Uid, s.Uid)) - } - if d.Gid != s.Gid { - log.Info(fmt.Sprintf("%s has GID %d should be %d", dest, d.Gid, s.Gid)) - } - if d.Mode != s.Mode { - log.Info(fmt.Sprintf("%s has mode %s should be %s", dest, os.FileMode(d.Mode), os.FileMode(s.Mode))) - } - if d.Md5 != s.Md5 { - log.Info(fmt.Sprintf("%s has md5sum %s should be %s", dest, d.Md5, s.Md5)) - } - if d.Uid != s.Uid || d.Gid != s.Gid || d.Mode != s.Mode || d.Md5 != s.Md5 { - return false, nil - } - return true, nil -} - -// recursiveFindFiles find files with pattern in the root with depth. -func recursiveFindFiles(root string, pattern string) ([]string, error) { - files := make([]string, 0) - findfile := func(path string, f os.FileInfo, err error) (inner error) { - if err != nil { - return - } - if f.IsDir() { - return - } else if match, innerr := filepath.Match(pattern, f.Name()); innerr == nil && match { - files = append(files, path) - } - return - } - err := filepath.Walk(root, findfile) - if len(files) == 0 { - return files, err - } else { - return files, err - } -} diff --git a/resource/template/fileStat_posix.go b/util/filestat_posix.go similarity index 74% rename from resource/template/fileStat_posix.go rename to util/filestat_posix.go index 7c4472972..f37826bb4 100644 --- a/resource/template/fileStat_posix.go +++ b/util/filestat_posix.go @@ -1,6 +1,6 @@ // +build !windows -package template +package util import ( "crypto/md5" @@ -11,9 +11,9 @@ import ( "syscall" ) -// fileStat return a fileInfo describing the named file. -func fileStat(name string) (fi fileInfo, err error) { - if isFileExist(name) { +// filestat return a FileInfo describing the named file. +func FileStat(name string) (fi FileInfo, err error) { + if IsFileExist(name) { f, err := os.Open(name) if err != nil { return fi, err diff --git a/resource/template/fileStat_windows.go b/util/filestat_windows.go similarity index 73% rename from resource/template/fileStat_windows.go rename to util/filestat_windows.go index 9e7c7df8e..65304f495 100644 --- a/resource/template/fileStat_windows.go +++ b/util/filestat_windows.go @@ -1,4 +1,4 @@ -package template +package util import ( "crypto/md5" @@ -8,8 +8,8 @@ import ( "os" ) -// fileStat return a fileInfo describing the named file. -func fileStat(name string) (fi fileInfo, err error) { +// filestat return a FileInfo describing the named file. +func FileStat(name string) (fi FileInfo, err error) { if isFileExist(name) { f, err := os.Open(name) if err != nil { diff --git a/util/util.go b/util/util.go new file mode 100644 index 000000000..bca81843f --- /dev/null +++ b/util/util.go @@ -0,0 +1,139 @@ +package util + +import ( + "fmt" + "github.com/kelseyhightower/confd/log" + "os" + "path" + "path/filepath" +) + +// Nodes is a custom flag Var representing a list of etcd nodes. +type Nodes []string + +// String returns the string representation of a node var. +func (n *Nodes) String() string { + return fmt.Sprintf("%s", *n) +} + +// Set appends the node to the etcd node list. +func (n *Nodes) Set(node string) error { + *n = append(*n, node) + return nil +} + +// fileInfo describes a configuration file and is returned by fileStat. +type FileInfo struct { + Uid uint32 + Gid uint32 + Mode os.FileMode + Md5 string +} + +func AppendPrefix(prefix string, keys []string) []string { + s := make([]string, len(keys)) + for i, k := range keys { + s[i] = path.Join(prefix, k) + } + return s +} + +// isFileExist reports whether path exits. +func IsFileExist(fpath string) bool { + if _, err := os.Stat(fpath); os.IsNotExist(err) { + return false + } + return true +} + +// IsConfigChanged reports whether src and dest config files are equal. +// Two config files are equal when they have the same file contents and +// Unix permissions. The owner, group, and mode must match. +// It return false in other cases. +func IsConfigChanged(src, dest string) (bool, error) { + if !IsFileExist(dest) { + return true, nil + } + d, err := FileStat(dest) + if err != nil { + return true, err + } + s, err := FileStat(src) + if err != nil { + return true, err + } + if d.Uid != s.Uid { + log.Info(fmt.Sprintf("%s has UID %d should be %d", dest, d.Uid, s.Uid)) + } + if d.Gid != s.Gid { + log.Info(fmt.Sprintf("%s has GID %d should be %d", dest, d.Gid, s.Gid)) + } + if d.Mode != s.Mode { + log.Info(fmt.Sprintf("%s has mode %s should be %s", dest, os.FileMode(d.Mode), os.FileMode(s.Mode))) + } + if d.Md5 != s.Md5 { + log.Info(fmt.Sprintf("%s has md5sum %s should be %s", dest, d.Md5, s.Md5)) + } + if d.Uid != s.Uid || d.Gid != s.Gid || d.Mode != s.Mode || d.Md5 != s.Md5 { + return true, nil + } + return false, nil +} + +func IsDirectory(path string) (bool, error) { + f, err := os.Stat(path) + if err != nil { + return false, err + } + switch mode := f.Mode(); { + case mode.IsDir(): + return true, nil + case mode.IsRegular(): + return false, nil + } + return false, nil +} + +func RecursiveFilesLookup(root string, pattern string) ([]string, error) { + return recursiveLookup(root, pattern, false) +} + +func RecursiveDirsLookup(root string, pattern string) ([]string, error) { + return recursiveLookup(root, pattern, true) +} + +func recursiveLookup(root string, pattern string, dirsLookup bool) ([]string, error) { + var result []string + isDir, err := IsDirectory(root) + if err != nil { + return nil, err + } + if isDir { + err := filepath.Walk(root, func(root string, f os.FileInfo, err error) error { + match, err := filepath.Match(pattern, f.Name()) + if err != nil { + return err + } + if match { + isDir, err := IsDirectory(root) + if err != nil { + return err + } + if isDir && dirsLookup { + result = append(result, root) + } else if !isDir && !dirsLookup { + result = append(result, root) + } + } + return nil + }) + if err != nil { + return nil, err + } + } else { + if !dirsLookup { + result = append(result, root) + } + } + return result, nil +} diff --git a/resource/template/util_test.go b/util/util_test.go similarity index 60% rename from resource/template/util_test.go rename to util/util_test.go index d9015b493..b7e62625e 100644 --- a/resource/template/util_test.go +++ b/util/util_test.go @@ -1,4 +1,4 @@ -package template +package util import ( "io/ioutil" @@ -20,14 +20,14 @@ import ( // │   ├── sub1.toml // │   └── sub12.toml // └── subDir2 -// ├── sub2.other -// ├── sub2.toml -// ├── sub22.toml -// └── subSubDir -// ├── subsub.other -// ├── subsub.toml -// └── subsub2.toml -func createRecursiveDirs() (rootDir string, err error) { +// ├── sub2.other +// ├── sub2.toml +// ├── sub22.toml +// └── subSubDir +// ├── subsub.other +// ├── subsub.toml +// └── subsub2.toml +func createDirStructure() (rootDir string, err error) { mod := os.FileMode(0755) flag := os.O_RDWR | os.O_CREATE | os.O_EXCL rootDir, err = ioutil.TempDir("", "") @@ -96,15 +96,15 @@ func createRecursiveDirs() (rootDir string, err error) { return } -func TestRecursiveFindFiles(t *testing.T) { +func TestRecursiveFilesLookup(t *testing.T) { log.SetLevel("warn") // Setup temporary directories - rootDir, err := createRecursiveDirs() + rootDir, err := createDirStructure() if err != nil { t.Errorf("Failed to create temp dirs: %s", err.Error()) } defer os.RemoveAll(rootDir) - files, err := recursiveFindFiles(rootDir, "*toml") + files, err := RecursiveFilesLookup(rootDir, "*toml") if err != nil { t.Errorf("Failed to run recursiveFindFiles, got error: " + err.Error()) } @@ -124,3 +124,61 @@ func TestRecursiveFindFiles(t *testing.T) { } } } + +func TestIsConfigChangedTrue(t *testing.T) { + log.SetLevel("warn") + src, err := ioutil.TempFile("", "src") + defer os.Remove(src.Name()) + if err != nil { + t.Errorf(err.Error()) + } + _, err = src.WriteString("foo") + if err != nil { + t.Errorf(err.Error()) + } + dest, err := ioutil.TempFile("", "dest") + defer os.Remove(dest.Name()) + if err != nil { + t.Errorf(err.Error()) + } + _, err = dest.WriteString("foo") + if err != nil { + t.Errorf(err.Error()) + } + status, err := IsConfigChanged(src.Name(), dest.Name()) + if err != nil { + t.Errorf(err.Error()) + } + if status == true { + t.Errorf("Expected IsConfigChanged(src, dest) to be %v, got %v", true, status) + } +} + +func TestIsConfigChangedFalse(t *testing.T) { + log.SetLevel("warn") + src, err := ioutil.TempFile("", "src") + defer os.Remove(src.Name()) + if err != nil { + t.Errorf(err.Error()) + } + _, err = src.WriteString("src") + if err != nil { + t.Errorf(err.Error()) + } + dest, err := ioutil.TempFile("", "dest") + defer os.Remove(dest.Name()) + if err != nil { + t.Errorf(err.Error()) + } + _, err = dest.WriteString("dest") + if err != nil { + t.Errorf(err.Error()) + } + status, err := IsConfigChanged(src.Name(), dest.Name()) + if err != nil { + t.Errorf(err.Error()) + } + if status == false { + t.Errorf("Expected sameConfig(src, dest) to be %v, got %v", false, status) + } +}