runtime/static_config.go (285 lines of code) (raw):
// Copyright (c) 2023 Uber Technologies, Inc.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
package zanzibar
import (
"encoding/json"
"io/ioutil"
"os"
"reflect"
"strconv"
"github.com/ghodss/yaml"
"github.com/mitchellh/mapstructure"
"github.com/pkg/errors"
)
type configType int
const (
filePathConfigType configType = 1
fileContentsConfigType configType = 2
)
// StaticConfig allows accessing values out of YAML(JSON) config files
type StaticConfig struct {
seedConfig map[string]interface{}
configOptions []*ConfigOption
configValues map[string]interface{}
frozen bool
destroyed bool
}
// ConfigOption points to a collection of bytes representing a file. This is
// either the full contents of the file as bytes, or a string poiting to a
// file
type ConfigOption struct {
configType configType
bytes []byte
}
// ConfigFilePath creates a ConfigFile represented as a path
func ConfigFilePath(path string) *ConfigOption {
return &ConfigOption{
configType: filePathConfigType,
bytes: []byte(path),
}
}
// ConfigFileContents creates a ConfigFile representing the contents of the file
func ConfigFileContents(fileBytes []byte) *ConfigOption {
return &ConfigOption{
configType: fileContentsConfigType,
bytes: fileBytes,
}
}
// NewStaticConfigOrDie allocates a static config instance
// StaticConfig takes two args, files and seedConfig.
//
// files is required and must be a list of file paths.
// The later files overwrite keys from earlier files.
//
// The seedConfig is optional and will be used to overwrite
// configuration files if present.
//
// The defaultConfig is optional initial config that will be overwritten by
// the config in the supplied files or the seed config
//
// The files must be a list of YAML files. Each file must be a flat object of
// key, value pairs. It's recommended that you use keys like:
//
// {
// "name": "my-name",
// "clients.thingy": {
// "some client": "config"
// },
// "server.my-port": 9999
// }
//
// To organize your configuration file.
func NewStaticConfigOrDie(
configOptions []*ConfigOption,
seedConfig map[string]interface{},
) *StaticConfig {
config := &StaticConfig{
configOptions: configOptions,
seedConfig: map[string]interface{}{},
configValues: map[string]interface{}{},
}
for key, value := range seedConfig {
config.seedConfig[key] = value
}
config.initializeConfigValues()
return config
}
func (conf *StaticConfig) checkConfDestroyed(key string) {
if conf.destroyed {
panic(errors.Errorf("cannot get(%s) because destroyed", key))
}
}
// MustGetBoolean returns the value as a boolean or panics.
func (conf *StaticConfig) MustGetBoolean(key string) bool {
conf.checkConfDestroyed(key)
if value, contains := conf.seedConfig[key]; contains {
return value.(bool)
}
if value, contains := conf.configValues[key]; contains {
return value.(bool)
}
panic(errors.Errorf("Key (%s) not available", key))
}
func mustConvertableToFloat(value interface{}, key string) float64 {
if v, ok := value.(int); ok {
return float64(v)
}
return value.(float64)
}
// MustGetFloat returns the value as a float or panics.
func (conf *StaticConfig) MustGetFloat(key string) float64 {
conf.checkConfDestroyed(key)
if value, contains := conf.seedConfig[key]; contains {
return mustConvertableToFloat(value, key)
}
if value, contains := conf.configValues[key]; contains {
return mustConvertableToFloat(value, key)
}
panic(errors.Errorf("Key (%s) not available", key))
}
func mustConvertableToInt(value interface{}, key string) int64 {
if v, ok := value.(float64); ok {
// value can be represented as an integer
if v != float64(int64(v)) {
panic(errors.Errorf("Key (%s) is a float", key))
}
return int64(v)
}
if v, ok := value.(int); ok {
return int64(v)
}
return value.(int64)
}
// MustGetInt returns the value as a int or panics.
func (conf *StaticConfig) MustGetInt(key string) int64 {
conf.checkConfDestroyed(key)
if value, contains := conf.seedConfig[key]; contains {
return mustConvertableToInt(value, key)
}
if value, contains := conf.configValues[key]; contains {
return mustConvertableToInt(value, key)
}
panic(errors.Errorf("Key (%s) not available", key))
}
// ContainsKey returns true if key is found otherwise false.
func (conf *StaticConfig) ContainsKey(key string) bool {
if conf.destroyed {
panic(errors.Errorf("Cannot ContainsKey(%s) because destroyed", key))
}
if _, contains := conf.seedConfig[key]; contains {
return true
}
if _, contains := conf.configValues[key]; contains {
return true
}
return false
}
// MustGetString returns the value as a string or panics.
func (conf *StaticConfig) MustGetString(key string) string {
conf.checkConfDestroyed(key)
if value, contains := conf.seedConfig[key]; contains {
return value.(string)
}
if value, contains := conf.configValues[key]; contains {
return value.(string)
}
panic(errors.Errorf("Key (%s) not available", key))
}
// MustGetStruct reads the value into an interface{} or panics.
// Recommended that this is used with pointers to structs
func (conf *StaticConfig) MustGetStruct(key string, ptr interface{}) {
conf.checkConfDestroyed(key)
rptr := reflect.ValueOf(ptr)
if rptr.Kind() != reflect.Ptr || rptr.IsNil() {
panic(errors.Errorf("Cannot GetStruct (%s) into nil ptr", key))
}
if v, contains := conf.seedConfig[key]; contains {
rptr.Elem().Set(reflect.ValueOf(v))
return
}
if v, contains := conf.configValues[key]; contains {
err := conf.mustGetStructHelper(key, v, ptr)
if err != nil {
panic(err)
}
return
}
panic(errors.Errorf("Key (%s) not available", key))
}
func (conf *StaticConfig) mustGetStructHelper(key string, v interface{}, ptr interface{}) error {
if value, ok := v.(map[string]interface{}); ok {
bytes, err := json.Marshal(value)
if err != nil {
panic(errors.Errorf("Decoding key (%s) failed", key))
}
err = json.Unmarshal(bytes, ptr)
if err != nil {
panic(errors.Errorf("Decoding key (%s) failed", key))
}
return nil
} else if value, ok := v.([]interface{}); ok {
err := mapstructure.Decode(value, ptr)
if err != nil {
panic(errors.Errorf("Decoding key (%s) failed", key))
}
return nil
}
return errors.Errorf("cant cast value into one of known types (%s)", key)
}
// SetConfigValueOrDie sets the static config value.
// dataType can be a boolean, number or string.
// SetConfigValueOrDie will panic if the config is frozen.
func (conf *StaticConfig) SetConfigValueOrDie(
key string, bytes []byte, dataType string) {
if conf.frozen {
panic(errors.Errorf("Cannot set(%s) because frozen", key))
}
var value interface{}
var err error
switch dataType {
case "boolean":
value, err = strconv.ParseBool(string(bytes))
case "number":
value, err = strconv.ParseFloat(string(bytes), 64)
case "string":
value, err = string(bytes), nil
default:
panic("unknown config data type")
}
if err != nil {
panic(errors.Errorf("Parsing config value (%s) falsed", string(bytes)))
}
conf.configValues[key] = value
}
// SetSeedOrDie a value in the config, useful for tests.
// Keys you set must not exist in the YAML files.
// Set() will panic if the key exists or if frozen.
// Strongly recommended not to be used for production code.
func (conf *StaticConfig) SetSeedOrDie(key string, value interface{}) {
if conf.frozen {
panic(errors.Errorf("Cannot set(%s) because frozen", key))
}
if _, contains := conf.configValues[key]; contains {
panic(errors.Errorf("Key (%s) already exists", key))
}
if _, contains := conf.seedConfig[key]; contains {
panic(errors.Errorf("Key (%s) already exists", key))
}
conf.seedConfig[key] = value
}
// Freeze the configuration store.
// Once you freeze the config any further calls to config.set() will panic.
// This allows you to make the static config immutable
func (conf *StaticConfig) Freeze() {
conf.frozen = true
}
// Destroy will make Get() calls fail with a panic.
// This allows you to terminate the configuration phase and gives you
// confidence that your application is now officially bootstrapped.
func (conf *StaticConfig) Destroy() {
conf.destroyed = true
conf.frozen = true
conf.configValues = map[string]interface{}{}
conf.seedConfig = map[string]interface{}{}
}
// InspectOrDie returns the entire config object.
// This should not be mutated and should only be used for inspection or debugging
func (conf *StaticConfig) InspectOrDie() map[string]interface{} {
result := map[string]interface{}{}
for k, v := range conf.configValues {
result[k] = v
}
for k, v := range conf.seedConfig {
result[k] = v
}
return result
}
// AsYaml returns a YAML serialized version of the StaticConfig
// If the StaticConfig is destroyed or frozen, this method returns an error
func (conf *StaticConfig) AsYaml() ([]byte, error) {
if conf.destroyed {
return nil, errors.New("error representing as YAML, config is destroyed")
}
if !conf.frozen {
return nil, errors.New("error representing as YAML, config is not frozen yet. Use Freeze() to mark the config as frozen")
}
yamlBytes, err := yaml.Marshal(conf.InspectOrDie())
if err != nil {
return nil, errors.Wrap(err, "error representing as YAML, failed to serialize values")
}
return yamlBytes, nil
}
func (conf *StaticConfig) initializeConfigValues() {
values := conf.collectConfigMaps()
conf.assignConfigValues(values)
}
func (conf *StaticConfig) collectConfigMaps() []map[string]interface{} {
maps := make([]map[string]interface{}, len(conf.configOptions))
for i := 0; i < len(conf.configOptions); i++ {
fileObject := conf.parseFile(conf.configOptions[i])
if fileObject != nil {
maps = append(maps, fileObject)
}
}
return maps
}
func (conf *StaticConfig) assignConfigValues(values []map[string]interface{}) {
for i := 0; i < len(values); i++ {
configObject := values[i]
for key, value := range configObject {
conf.configValues[key] = value
}
}
}
func (conf *StaticConfig) parseFile(
configFile *ConfigOption,
) map[string]interface{} {
var bytes []byte
switch configFile.configType {
case filePathConfigType:
var err error
bytes, err = ioutil.ReadFile(string(configFile.bytes))
if err != nil {
if os.IsNotExist(err) {
// Ignore missing files
return nil
}
// If the ReadFile() failed then just panic out.
panic(err)
}
case fileContentsConfigType:
bytes = configFile.bytes
default:
panic(errors.Errorf(
"Unknown config file type %d",
configFile.configType,
))
}
var object map[string]interface{}
err := yaml.Unmarshal(bytes, &object)
if err != nil {
panic(err)
}
return object
}