konfig

package
v0.1.3 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 3, 2023 License: Apache-2.0 Imports: 11 Imported by: 0

README

konfig

About

Rather than another config-parsing library, instead konfig aims to:

  • load configurations from one centralized config provider
  • hot reload & unmarshalling on configuration change
  • encourage users to access configs via a single strong typing config instance

Supported config format (as for now):

  • json
  • yaml

Supported config source:

  • File
  • Consul
  • Etcd
  • Nacos

Getting started

Example code can be found at cmd/konfig-example.

1. Implement konfig.ConfigProxy

NOTE

  1. The definition is pretty static, just copy and paste.

config/main.go

package config

import (
     "github.com/mykube-run/kindling/pkg/konfig"
)

// Proxy konfig proxy
// --------------------
var Proxy = &proxy{c: new(Sample)}

type proxy struct {
     c *Sample
}

func (p *proxy) Get() interface{} {
     return *p.c
}

func (p *proxy) Populate(fn func(interface{}) error) error {
     return fn(p.c)
}

func (p *proxy) New() konfig.ConfigProxy {
     return &proxy{c: new(Sample)}
}

2. Define config struct Sample

NOTE

  1. Accessing config blocks via an exported function is recommended (also the best practice), e.g. config.DB()
  2. Always access current config from Proxy instance, which has the latest version of config, e.g. Proxy.Get().(Sample).DB
  3. Field tags are declared via mapstructure.

config/konfig.go

package config

// Sample is a config sample
type Sample struct {
     API         APIConfig         `mapstructure:"api"`
     DB          DatabaseConfig    `mapstructure:"db"`
     FeatureGate FeatureGateConfig `mapstructure:"feature_gate"`
}

// API returns the API config
func API() APIConfig {
     return Proxy.Get().(Sample).API
}

type APIConfig struct {
     ServiceXXXAddress string `mapstructure:"service_xxx_address"`
}

// DB returns the database config
func DB() DatabaseConfig {
     return Proxy.Get().(Sample).DB
}

type DatabaseConfig struct {
     Address  string `mapstructure:"address"`
     Username string `mapstructure:"username"`
     Password string `mapstructure:"password"`
}

// FeatureGate returns the feature gate config
func FeatureGate() FeatureGateConfig {
     return Proxy.Get().(Sample).FeatureGate
}

type FeatureGateConfig struct {
     EnableXXX bool `mapstructure:"enable_xxx"`
}

3. Implement konfig.ConfigUpdateHandler

handler.go

package main

import (
     "fmt"
     "github.com/mykube-run/kindling/cmd/konfig-example/config"
     "github.com/mykube-run/kindling/pkg/konfig"
)

// db demonstrates how database is reconnected,
// it would be a DB SINGLETON POINTER in real code
var db string

var hdl1 = konfig.ConfigUpdateHandler{
     Name: "database",
     Handle: func(prev, cur interface{}) error {
          fmt.Printf("* previous config: %+v\n", prev)
          fmt.Printf("* current config: %+v\n", cur)
          pc, _ := prev.(config.Sample)
          cc, _ := cur.(config.Sample)

          if cc.DB.Address != pc.DB.Address {
               if pc.DB.Address == "" {
                    fmt.Printf("* database address was set, connecting database...\n")
                    db = cc.DB.Address
                    return nil
               }
               fmt.Printf("* database address was updated, reconnecting database...\n")
               db = cc.DB.Address
          }
          return nil
     },
}

var hdl2 = konfig.ConfigUpdateHandler{
     Name: "feature gate",
     Handle: func(prev, cur interface{}) error {
          pc, _ := prev.(config.Sample)
          cc, _ := cur.(config.Sample)

          if cc.FeatureGate.EnableXXX != pc.FeatureGate.EnableXXX &&
                  cc.FeatureGate.EnableXXX {
               fmt.Printf("* feature xxx was enabled, updating dependencies...\n")
          }
          return nil
     },
}
4. Implement business code

main.go

package main

import (
     "fmt"
     "github.com/mykube-run/kindling/cmd/konfig-example/config"
     "github.com/mykube-run/kindling/pkg/konfig"
     "log"
     "time"
)

func main() {
     _, err := konfig.New(config.Proxy, hdl1, hdl2)
     if err != nil {
          log.Fatalf("failed to create new config manager: %v", err)
     }

     for {
          time.Sleep(5 * time.Second)
          // Accessing db via singleton
          fmt.Printf("* accessing database: %v...\n", db)
          // Accessing config values directly
          fmt.Printf("* calling service xxx via address: %v...\n", config.API().ServiceXXXAddress)
          if config.FeatureGate().EnableXXX {
               fmt.Printf("* feature xxx was enabled, performing actions accordingly...\n")
          }
     }
}

5. Create config

Start a config source, e.g. Consul, Etcd, Nacos (or create a config file). Populate initial config with following value:

{
  "api": {
    "service_xxx_address": "localhost:8000"
  },
  "db": {
    "address": "localhost:3306",
    "username": "test",
    "password": "test"
  },
  "feature_gate": {
    "enable_xxx": false
  }
}
6. Start the demo
# Command line flags
go run ./cmd/konfig-example --conf-addr='localhost:8848' --conf-format=json \
  --conf-namespace=konfig --conf-group=test --conf-key=konfig-test --conf-type=nacos

# Env
CONF_ADDR='localhost:8500' CONF_FORMAT=json CONF_KEY=konfig-test CONF_TYPE=consul \
  go run ./cmd/konfig-example

Observe log output:

* previous config: {API:{ServiceXXXAddress:} DB:{Address: Username: Password:} FeatureGate:{EnableXXX:false}}
* current config: {API:{ServiceXXXAddress:localhost:8000} DB:{Address:localhost:3306 Username:test Password:test} FeatureGate:{EnableXXX:false}}
* database address was set, connecting database...

{"level":"trace","module":"konfig","time":"2022-03-08T14:37:52+08:00","message":"handler [database] finished"}
{"level":"trace","module":"konfig","time":"2022-03-08T14:37:52+08:00","message":"handler [feature gate] finished"}
{"level":"info","module":"konfig","time":"2022-03-08T14:37:52+08:00","message":"updated config, md5: 900b5e06b577f1b8025e5502e048bdc6"}

* accessing database: localhost:3306...
* calling service xxx via address: localhost:8000...
7. Update config and observe log output
{"level":"trace","time":"2022-03-08T14:38:36+08:00","message":"namespace: konfig, group: test, key: konfig-test, md5: c8804a37eccf246fd87706c9eb1e7495"}
* previous config: {API:{ServiceXXXAddress:localhost:8000} DB:{Address:localhost:3306 Username:test Password:test} FeatureGate:{EnableXXX:false}}
* current config: {API:{ServiceXXXAddress:localhost:8080} DB:{Address:localhost:13306 Username:test Password:test} FeatureGate:{EnableXXX:true}}

* database address was updated, reconnecting database...
{"level":"trace","time":"2022-03-08T14:38:36+08:00","message":"handler [database] finished"}

* feature xxx was enabled, updating dependencies...
{"level":"trace","time":"2022-03-08T14:38:36+08:00","message":"handler [feature gate] finished"}
{"level":"info","time":"2022-03-08T14:38:36+08:00","message":"updated config, md5: c8804a37eccf246fd87706c9eb1e7495"}

* accessing database: localhost:13306...
* calling service xxx via address: localhost:8080...
* feature xxx was enabled, performing actions accordingly...

Documentation

Index

Constants

This section is empty.

Variables

View Source
var NOOPHandler = ConfigUpdateHandler{
	Name: "noop",
	Handle: func(prev, cur interface{}) error {
		return nil
	},
}

NOOPHandler does nothing on config update

View Source
var NewBootstrapOptionFromEnvFlag = func() *BootstrapOption {
	opt := NewBootstrapOption()
	opt.parseEnvFlags()
	return opt
}

NewBootstrapOptionFromEnvFlag initializes a bootstrap config option from environments & flags. Flag value has higher priority when both given in environments & flags. NOTE:

  1. Flags are parsed once this function is called.
  2. Customize this function if needed

Functions

func NewConfigSource

func NewConfigSource(opt *BootstrapOption) (source.ConfigSource, error)

Types

type BootstrapOption

type BootstrapOption struct {
	Type            source.ConfigSourceType
	Format          string
	Addrs           []string
	Namespace       string
	Group           string
	Key             string
	MinimalInterval time.Duration
	Logger          log.Logger
}

BootstrapOption is used to specify config source (and other additional) options.

func NewBootstrapOption

func NewBootstrapOption() *BootstrapOption

NewBootstrapOption initializes a bootstrap config option

func (*BootstrapOption) Validate

func (opt *BootstrapOption) Validate() error

Validate checks option values

func (*BootstrapOption) WithAddr

func (opt *BootstrapOption) WithAddr(addr string) *BootstrapOption

WithAddr adds an address to the option

func (*BootstrapOption) WithAddrs

func (opt *BootstrapOption) WithAddrs(addrs []string) *BootstrapOption

WithAddrs replaces option's addrs with given value

func (*BootstrapOption) WithGroup

func (opt *BootstrapOption) WithGroup(group string) *BootstrapOption

WithGroup specifies config group

func (*BootstrapOption) WithIpPort

func (opt *BootstrapOption) WithIpPort(ip, port interface{}) *BootstrapOption

WithIpPort adds an ip:port address to the option

func (*BootstrapOption) WithKey

func (opt *BootstrapOption) WithKey(key string) *BootstrapOption

WithKey specifies config key

func (*BootstrapOption) WithLogger

func (opt *BootstrapOption) WithLogger(lg log.Logger) *BootstrapOption

WithLogger specifies a custom logger to the option

func (*BootstrapOption) WithMinimalInterval

func (opt *BootstrapOption) WithMinimalInterval(v time.Duration) *BootstrapOption

WithMinimalInterval specifies a minimal duration that config can be updated, defaults to 5s. This prevents your application being destroyed by event storm.

func (*BootstrapOption) WithNamespace

func (opt *BootstrapOption) WithNamespace(ns string) *BootstrapOption

WithNamespace specifies config namespace

func (*BootstrapOption) WithType

WithType specifies config source type

type ConfigProxy

type ConfigProxy interface {
	// Get always returns current config value holding by ConfigProxy
	Get() interface{}
	// Populate takes a closure function as input, fills new config data into its config value
	Populate(func(interface{}) error) error
	// New creates a ConfigProxy with an empty config instance
	New() ConfigProxy
}

ConfigProxy is a proxy for user's business config, defining the way user's config is accessed & modified by konfig.Manager, as well as how to generate a brand-new config proxy.

When configuration was updated, a kconfig manager will: 1) Create a new temporary proxy using ConfigProxy.New 2) Wrap new config data (in bytes) in a closure function, then calls ConfigProxy.Populate 3) Call update handlers one by one 4) Call manager.ConfigProxy.Populate to update existing config

type ConfigUpdateHandler

type ConfigUpdateHandler struct {
	Name   string
	Handle func(prev, cur interface{}) error
}

ConfigUpdateHandler is called when config change, it enables user to compare the new config with previous one, and decide what kind of action should be taken, e.g. reconnect database, refresh cache or send a notification.

type Manager

type Manager struct {
	// contains filtered or unexported fields
}

func New

func New(proxy ConfigProxy, hdl ...ConfigUpdateHandler) (*Manager, error)

New creates a new Manager instance, which will automatically read BootstrapOption from environment & flags. Once created, Manager will read config data and update config with the help of types.ConfigProxy. hdl is a series of custom update handlers, will be called sequentially after Manager is created and when config is changed.

func NewWithOption

func NewWithOption(proxy ConfigProxy, opt *BootstrapOption, hdl ...ConfigUpdateHandler) (*Manager, error)

NewWithOption creates a new Manager instance with given BootstrapOption.

func (*Manager) Register

func (m *Manager) Register(hdl ...ConfigUpdateHandler) *Manager

Register registers extra event handlers after creation

Directories

Path Synopsis

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL