config/monkey.go (313 lines of code) (raw):

// Copyright 2016 Netflix, Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package config import ( "encoding/json" "fmt" "io" "log" "math" "os" "strings" "time" "github.com/pkg/errors" "github.com/spf13/pflag" "github.com/spf13/viper" "github.com/Netflix/chaosmonkey/v2/config/param" ) // Monkey is is a config implementation backed by viper type Monkey struct { remote bool // if true, there's a remote provider v *viper.Viper } const ( clockStartHour int = 0 clockEndHour int = 23 hoursInClock int = 24 cronBeforeStartHour int = 2 ) func (m *Monkey) setDefaults() { m.v.SetDefault(param.Enabled, false) m.v.SetDefault(param.Leashed, true) m.v.SetDefault(param.ScheduleEnabled, false) m.v.SetDefault(param.Accounts, []string{}) m.v.SetDefault(param.StartHour, 9) m.v.SetDefault(param.EndHour, 15) m.v.SetDefault(param.TimeZone, "America/Los_Angeles") m.v.SetDefault(param.CronPath, "/etc/cron.d/chaosmonkey-daily-terminations") m.v.SetDefault(param.TermPath, "/apps/chaosmonkey/chaosmonkey-terminate.sh") m.v.SetDefault(param.TermAccount, "root") m.v.SetDefault(param.MaxApps, math.MaxInt32) m.v.SetDefault(param.Trackers, []string{}) m.v.SetDefault(param.Decryptor, "") m.v.SetDefault(param.OutageChecker, "") m.v.SetDefault(param.DatabasePort, 3306) m.v.SetDefault(param.SpinnakerEndpoint, "") m.v.SetDefault(param.SpinnakerCertificate, "") m.v.SetDefault(param.SpinnakerEncryptedPassword, "") m.v.SetDefault(param.SpinnakerUser, "") m.v.SetDefault(param.SpinnakerX509Cert, "") m.v.SetDefault(param.SpinnakerX509Key, "") m.v.SetDefault(param.DynamicProvider, "") m.v.SetDefault(param.DynamicEndpoint, "") m.v.SetDefault(param.DynamicPath, "") m.v.SetDefault(param.ScheduleCronPath, "/etc/cron.d/chaosmonkey-schedule") m.v.SetDefault(param.SchedulePath, "/apps/chaosmonkey/chaosmonkey-schedule.sh") m.v.SetDefault(param.LogPath, "/var/log") } func (m *Monkey) setupEnvVarReader() { // read from environment variables m.v.AutomaticEnv() // Replace "." with "_" when reading environment variables // e.g.: chaosmonkey.enabled -> CHAOSMONKEY_ENABLED m.v.SetEnvKeyReplacer(strings.NewReplacer(".", "_")) } // Load returns a Monkey config that loads config from a file func Load(configPaths []string) (*Monkey, error) { m := &Monkey{v: viper.New()} m.setDefaults() m.setupEnvVarReader() for _, dir := range configPaths { m.v.AddConfigPath(dir) } m.v.SetConfigType("toml") m.v.SetConfigName("chaosmonkey") err := m.v.ReadInConfig() // It's ok if the config file doesn't exist, but we want to catch any // other config-related issues if err != nil { if !os.IsNotExist(err) { return nil, errors.Wrapf(err, "failed to read config file") } log.Printf("no config file found, proceeding without one") } err = m.configureRemote() if err != nil { return nil, err } return m, nil } // Defaults returns a Monkey config that just has the default values set // it will not load local files or remote ones func Defaults() *Monkey { v := &Monkey{v: viper.New()} v.setDefaults() return v } // NewFromReader returns a Monkey config which parses the initial config // from a reader. It may load remote if configured to // Config file must be in toml format func NewFromReader(in io.Reader) (*Monkey, error) { m := &Monkey{v: viper.New()} m.setDefaults() m.v.SetConfigType("toml") err := m.v.ReadConfig(in) if err != nil { return nil, errors.Wrap(err, "failed to parse config") } err = m.configureRemote() if err != nil { return nil, err } return m, nil } // configureRemote configures viper for a remote provider if the user has // specified one func (m *Monkey) configureRemote() error { provider := m.v.GetString(param.DynamicProvider) endpoint := m.v.GetString(param.DynamicEndpoint) path := m.v.GetString(param.DynamicPath) // If the user specified an external provider, use it if provider != "" { m.remote = true m.v.SetConfigType("json") err := m.v.AddRemoteProvider(provider, endpoint, path) if err != nil { return errors.Wrapf(err, "failed viper.AddRemoteProvider(provider=\"%s\", endpoint=\"%s\", path=\"%s\"):", provider, endpoint, path) } } return nil } // SetRemoteProvider sets remote configuration parameters. // These will typically be set by parsing the config files. This method // exists to facilitate testing func (m *Monkey) SetRemoteProvider(provider string, endpoint string, path string) error { m.v.Set(param.DynamicProvider, provider) m.v.Set(param.DynamicEndpoint, endpoint) m.v.Set(param.DynamicPath, path) return m.configureRemote() } // Set overrides the config value. Used for testing func (m *Monkey) Set(key string, value interface{}) { m.v.Set(key, value) } // readRemoteConfig retrieves config parameters from a remote source // If no remote source has been configured, this is a no-op func (m *Monkey) readRemoteConfig() error { if !m.remote { return nil } return m.v.ReadRemoteConfig() } // Enabled returns true if Chaos Monkey is enabled func (m *Monkey) Enabled() (bool, error) { return m.getDynamicBool(param.Enabled) } // Leashed returns true if Chaos Monkey is leashed // In leashed mode, Chaos Monkey records terminations but does not actually // terminate func (m *Monkey) Leashed() (bool, error) { return m.getDynamicBool(param.Leashed) } // ScheduleEnabled returns true if Chaos Monkey termination scheduling is enabled // if false, Chaos Monkey will not generate a termination schedule func (m *Monkey) ScheduleEnabled() (bool, error) { return m.getDynamicBool(param.ScheduleEnabled) } func (m *Monkey) getDynamicBool(param string) (bool, error) { err := m.readRemoteConfig() if err != nil { return false, err } return m.v.GetBool(param), nil } // AccountEnabled returns true if Chaos Monkey is enabled for that account func (m *Monkey) AccountEnabled(account string) (bool, error) { accounts, err := m.Accounts() if err != nil { return false, err } for _, x := range accounts { if account == x { return true, nil } } return false, nil } // Accounts return a list of accounts where Choas Monkey is enabled func (m *Monkey) Accounts() ([]string, error) { err := m.readRemoteConfig() if err != nil { return nil, err } return m.getStringSlice(param.Accounts) } // toStrings converts a slice of interfaces to a slice of strings func toStrings(values []interface{}) ([]string, error) { result := make([]string, len(values)) for i, x := range values { x, valid := x.(string) if !valid { return nil, errors.Errorf("non-string in %v", values) } result[i] = x } return result, nil } // StartHour (o'clock) is when Chaos // Monkey starts terminating this value is in [0,23] This is time-zone // dependent, see the Location method func (m *Monkey) StartHour() int { return m.v.GetInt(param.StartHour) } // EndHour (o'clock) is the time after which Chaos Monkey will // not terminate instances. // this value is in [0,23] // This is time-zone dependent, see the Location method func (m *Monkey) EndHour() int { return m.v.GetInt(param.EndHour) } // Location returns the time zone of StartHour and EndHour. // May return an error if time.LoadLocation fails func (m *Monkey) Location() (*time.Location, error) { return time.LoadLocation(m.v.GetString(param.TimeZone)) } // CronPath returns the path to where Chaos Monkey // puts the cron job file with daily terminations func (m *Monkey) CronPath() string { return m.v.GetString(param.CronPath) } // TermPath returns the path to the executable that // wraps the chaos monkey binary for terminating instances func (m *Monkey) TermPath() string { return m.v.GetString(param.TermPath) } // TermAccount returns the account that cron will use // to execute the termination command func (m *Monkey) TermAccount() string { return m.v.GetString(param.TermAccount) } // MaxApps returns the maximum number of apps to // examine for termination func (m *Monkey) MaxApps() int { return m.v.GetInt(param.MaxApps) } // Trackers returns the names of the backend implementation for // termination trackers. Used for things like logging and metrics collection func (m *Monkey) Trackers() ([]string, error) { return m.getStringSlice(param.Trackers) } // ErrorCounter returns the names of the backend implementions for // error counters. Intended for monitoring/alerting. func (m *Monkey) ErrorCounter() string { return m.v.GetString(param.ErrorCounter) } func (m *Monkey) getStringSlice(key string) ([]string, error) { // This could be encoded natively as a list of strings, or as a string that // represents a list of strings, so we need to handle both cases t := m.v.Get(key) if t == nil { return nil, fmt.Errorf("%s not specified", param.Accounts) } switch t := t.(type) { default: return nil, fmt.Errorf("%s: unexpected type %T", param.Accounts, t) case []string: // When set explicitly in code return t, nil case []interface{}: // When reading from config file return toStrings(t) case string: // When reading from prana, which uses string encoding // Convert to list of strings var result []string err := json.Unmarshal([]byte(t), &result) return result, err } } // SpinnakerEndpoint returns the spinnaker endpoint func (m *Monkey) SpinnakerEndpoint() string { return m.v.GetString(param.SpinnakerEndpoint) } // SpinnakerCertificate retunrs a path to a .p12 file that contains a TLS cert // for authenticating against Spinnaker func (m *Monkey) SpinnakerCertificate() string { return m.v.GetString(param.SpinnakerCertificate) } // SpinnakerEncryptedPassword returns an password that // is used to decrypt the Spinnaker certificate. The encryption scheme // is defined by the Decryptor parameter func (m *Monkey) SpinnakerEncryptedPassword() string { return m.v.GetString(param.SpinnakerEncryptedPassword) } // SpinnakerUser is sent in the "user" field in the terminateInstances task sent // to Spinnaker when Spinnaker terminates an instance func (m *Monkey) SpinnakerUser() string { return m.v.GetString(param.SpinnakerUser) } // SpinnakerX509Cert retunrs a path to a X509 cert file func (m *Monkey) SpinnakerX509Cert() string { return m.v.GetString(param.SpinnakerX509Cert) } // SpinnakerX509Key retunrs a path to a X509 key file func (m *Monkey) SpinnakerX509Key() string { return m.v.GetString(param.SpinnakerX509Key) } // Decryptor returns an interface for decrypting secrets func (m *Monkey) Decryptor() string { return m.v.GetString(param.Decryptor) } // OutageChecker returns an interface for checking if there is an ongoing // outage func (m *Monkey) OutageChecker() string { return m.v.GetString(param.OutageChecker) } // DatabaseHost returns the hostname the database is running on func (m *Monkey) DatabaseHost() string { return m.v.GetString(param.DatabaseHost) } // DatabasePort returns the port the database is listening on func (m *Monkey) DatabasePort() int { return m.v.GetInt(param.DatabasePort) } // DatabaseUser returns the database user associated with the credentials func (m *Monkey) DatabaseUser() string { return m.v.GetString(param.DatabaseUser) } // DatabaseName returns the name of the database that stores the Chaos Monkey // state func (m *Monkey) DatabaseName() string { return m.v.GetString(param.DatabaseName) } // DatabaseEncryptedPassword returns an encrypted version of the database // credentials func (m *Monkey) DatabaseEncryptedPassword() string { return m.v.GetString(param.DatabaseEncryptedPassword) } // BindPFlag binds a specific parameter to a pflag func (m *Monkey) BindPFlag(parameter string, flag *pflag.Flag) (err error) { return m.v.BindPFlag(parameter, flag) } // The code below is to provide a mechanism for adding a new remote config // provider without directly viper. Viper wasn't designed for this use-case // so this is a workaround. // RemoteProvider is a type alias type RemoteProvider viper.RemoteProvider // RemoteConfigFactory is the same interface as viper.remoteConfigFactory // This is a workaround to be able to support backends other than etc/consul // without modifying viper type RemoteConfigFactory interface { Get(rp RemoteProvider) (io.Reader, error) Watch(rp RemoteProvider) (io.Reader, error) } type proxy struct { factory RemoteConfigFactory } func (p proxy) Get(rp viper.RemoteProvider) (io.Reader, error) { return p.factory.Get(rp) } func (p proxy) Watch(rp viper.RemoteProvider) (io.Reader, error) { return p.factory.Watch(rp) } // SetRemoteProvider sets viper's remote provider func SetRemoteProvider(name string, factory RemoteConfigFactory) { viper.RemoteConfig = proxy{factory} viper.SupportedRemoteProviders = []string{name} } // CronExpression returns the chaosmonkey main run cron expression. // It defaults to 2 hour before start_hour on weekdays, if no cron expression // is specified in the config func (m *Monkey) CronExpression() (string, error) { defaultCron := "0 %d * * 1-5" cron := m.v.Get(param.CronExpression) if cron == nil { runAtHour, err := calculateDefaultCronRunHour(m.StartHour()) if err != nil { return "", err } return fmt.Sprintf(defaultCron, runAtHour), nil } switch cron := cron.(type) { default: return "", fmt.Errorf("%s: unexpected type %T", param.CronExpression, cron) case string: return cron, nil } } // calculates the default cron run hour based on startHour. // The default cron starts "cronBeforeStartHour" hours // before "startHour" func calculateDefaultCronRunHour(startHour int) (int, error) { if (startHour < clockStartHour) || (startHour > clockEndHour) { return -1, errors.Errorf("%d is not in cron range(0-23)", startHour) } runAtHour := startHour - cronBeforeStartHour if runAtHour < 0 { // assuming a 24 hour clock system(0 - 23), -ve values means going back to previous day // e.g. if start hour is 0 (midnight), the "cronTime" time should be 22 hours // on the previous day. return hoursInClock + runAtHour, nil } return runAtHour, nil } // ScheduleCronPath returns the path to which // main chaosmonkey crontab is located func (m *Monkey) ScheduleCronPath() string { return m.v.GetString(param.ScheduleCronPath) } // SchedulePath returns the path to which main // chaosmonkey schedule script(invoked from cron) is located func (m *Monkey) SchedulePath() string { return m.v.GetString(param.SchedulePath) } // LogPath returns the path to which // log files should be written func (m *Monkey) LogPath() string { return m.v.GetString(param.LogPath) }