Skip to content

Instantly share code, notes, and snippets.

@hynek
Last active November 26, 2016 19:10
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save hynek/78fef47fea3cbc6b683cd8ab0fb68567 to your computer and use it in GitHub Desktop.
Save hynek/78fef47fea3cbc6b683cd8ab0fb68567 to your computer and use it in GitHub Desktop.
PoC of using attrs’s upcoming metadata feature for using declarative application configuration from env variables including envconsul’s HashiCorp Vault support.
from __future__ import absolute_import, division, print_function
from app_config import (app_config, env_var, env_var_from_vault,
env_var_group, environ_to_app_config)
@app_config(prefix="APP", vault_prefix="WHOIS_{env}")
class WhoisConfig(object):
@app_config
class Prometheus(object):
address = env_var(default="127.0.0.1")
port = env_var(default="0")
consul_token = env_var_from_vault()
env = env_var()
prometheus = env_var_group(Prometheus)
if __name__ == '__main__':
ac = environ_to_app_config(WhoisConfig)
print(ac)
import os
import attr
CNF_KEY = object()
RAISE = object()
class ConfigError(Exception):
pass
class MissingEnvValueError(ConfigError):
pass
def app_config(maybe_cls=None, vault_prefix=None, prefix="APP"):
def wrap(cls):
cls._vault_prefix = vault_prefix
cls._prefix = prefix
return attr.s(cls, slots=True, frozen=True)
if maybe_cls is None:
return wrap
else:
return wrap(maybe_cls)
@attr.s(slots=True, frozen=True)
class _ConfigEntry(object):
default = attr.ib(default=RAISE)
sub_cls = attr.ib(default=None)
from_vault = attr.ib(default=False)
def env_var(default=RAISE):
return attr.ib(
default=default,
metadata={CNF_KEY: _ConfigEntry(default, None, False)}
)
def env_var_from_vault(default=RAISE):
return attr.ib(
default=default,
metadata={CNF_KEY: _ConfigEntry(default, None, True)}
)
def env_var_group(cls):
return attr.ib(
default=None,
metadata={CNF_KEY: _ConfigEntry(None, cls, True)}
)
def environ_to_app_config(cls, environ=os.environ,
prefix=None, vault_prefix=None):
if prefix is None:
prefix = cls._prefix
if vault_prefix is None:
vault_prefix = cls._vault_prefix
vals = {}
for a in attr.fields(cls):
name = a.name.upper()
cm = a.metadata[CNF_KEY]
if cm.sub_cls is None:
if cm.from_vault is False:
var = prefix + "_" + name
else:
var = "SECRET_" + vault_prefix + "_" + prefix + "_" + name
val = environ.get(var, cm.default)
if val is RAISE:
raise MissingEnvValueError(var)
if name == "ENV" and "{env}" in vault_prefix:
vault_prefix = vault_prefix.replace("{env}", val.upper())
else:
val = environ_to_app_config(
cm.sub_cls, environ, prefix + "_" + name, vault_prefix,
)
vals[a.name] = val
return cls(**vals)
$ env APP_ENV=dev APP_PROMETHEUS_PORT=7000 SECRET_WHOIS_DEV_APP_PROMETHEUS_CONSUL_TOKEN=abc python app.py
WhoisConfig(env='dev', prometheus=Prometheus(address='127.0.0.1', port='7000', consul_token='abc'))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment