diff --git a/.github/workflows/publish-to-test-pypi.yml b/.github/workflows/publish-to-test-pypi.yml new file mode 100644 index 0000000..a6bbc93 --- /dev/null +++ b/.github/workflows/publish-to-test-pypi.yml @@ -0,0 +1,33 @@ +name: Publish Envars to PyPi + +on: push + +jobs: + build-n-publish: + name: Build and publish to PyPi + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: "3.x" + - name: Install pypa/build + run: >- + python -m + pip install + build + --user + - name: Build a binary wheel and a source tarball + run: >- + python -m + build + --sdist + --wheel + --outdir dist/ + . + - name: Publish distribution 📦 to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 + with: + password: ${{ secrets.PYPI_API_TOKEN }} diff --git a/envars/envars.py b/envars/envars.py index adffbb3..d27988c 100755 --- a/envars/envars.py +++ b/envars/envars.py @@ -115,6 +115,13 @@ def main(): required=False, action='store_true', ) + parser_print.add_argument( + '-n', + '--no-templating', + required=False, + default=False, + action='store_true', + ) parser_print.add_argument( '-e', '--env', @@ -147,6 +154,13 @@ def main(): default=False, action='store_true', ) + parser_print.add_argument( + '-S', + '--secrets_only', + required=False, + default=False, + action='store_true', + ) parser_print.set_defaults(func=print_env) # @@ -248,7 +262,17 @@ def set_systemd_env(args): if 'RELEASE_SHA' in os.environ: args.template_var = [f'RELEASE={os.environ.get("RELEASE_SHA")}'] args.var = None - ret = process(args) + ret = process( + args.filename, + args.account, + args.env, + args.var, + args.template_var, + args.decrypt, + True if args.no_templating is False else False, + args.yaml, + args.quote, + ) for val in ret: parts = val.split("=", 1) subprocess.run( @@ -277,7 +301,17 @@ def execute(args): vals = envars.get_var(args.var, args.env, args.account) else: args.var = None - ret = process(args) + ret = process( + args.filename, + args.account, + args.env, + args.var, + args.template_var, + args.decrypt, + True if args.no_templating is False else False, + args.yaml, + args.quote, + ) for val in ret: parts = val.split("=", 1) vals[parts[0]] = parts[1] @@ -346,7 +380,18 @@ def add_var(args): def print_env(args): - ret = process(args) + ret = process( + args.filename, + args.account, + args.env, + args.var, + args.template_var, + args.decrypt, + True if args.no_templating is False else False, + args.yaml, + args.quote, + args.secrets_only, + ) if isinstance(ret, list): for var in ret: print(var) @@ -354,28 +399,35 @@ def print_env(args): print(ret) -def process(args): - envars = EnVars(args.filename) +def process( + filename, account, env, + var=None, template_var=None, decrypt=False, + templating=True, as_yaml=False, quote=False, secrets_only=False): + + envars = EnVars(filename) envars.load() - if args.account is None: + if account is None: account = get_account() else: - account = args.account + account = account - if args.env: + if env: template_vars = {} - for tvar in flatten(args.template_var): - template_vars[tvar.split('=')[0]] = tvar.split('=')[1] + if template_var: + for tvar in flatten(template_var): + template_vars[tvar.split('=')[0]] = tvar.split('=')[1] - if args.yaml: + if as_yaml: return ( yaml.dump( {'envars': envars.build_env( - args.env, + env, account, - decrypt=args.decrypt, + decrypt=decrypt, template_vars=template_vars, + templating=templating, + secrets_only=secrets_only, )}, default_flow_style=False ) @@ -383,21 +435,22 @@ def process(args): else: env_vars = [] for name, value in envars.build_env( - args.env, + env, account, - decrypt=args.decrypt, - template_vars=template_vars).items(): - if args.quote: + decrypt=decrypt, + template_vars=template_vars, + secrets_only=secrets_only, + templating=templating).items(): + if quote: env_vars.append(f"{name}='{value}'") else: env_vars.append(f'{name}={value}') return env_vars else: - var = None - if args.var: - var = args.var.upper() - return (envars.print(account, var=var, decrypt=args.decrypt)) + if var: + var = var.upper() + return (envars.print(account, var=var, decrypt=decrypt)) def flatten(lis): diff --git a/envars/models.py b/envars/models.py index 89cded6..c51c625 100755 --- a/envars/models.py +++ b/envars/models.py @@ -210,32 +210,44 @@ def get_var(self, var, env, account): if v.name == var: return {v.name: v.get_value(env, account, fetch_pstore=True)} - def build_env(self, env, account, decrypt=False, template_vars=None): + def build_env(self, env, account, decrypt=False, template_vars=None, templating=True, secrets_only=False): logging.debug(f'build_env({env}, {account})') envars = {} if env != 'default' and env not in self.envs: raise (Exception(f'Unknown Env: "{env}"')) - template_vars['STAGE'] = env - # fetch all the non secret values - for v in self.envars: - value = v.get_value(env, account, fetch_pstore=True) - if value and not isinstance(value, Secret): - if v.name not in template_vars.keys(): - template_vars[v.name] = value - envars[v.name] = value + if templating and not secrets_only: + template_vars['STAGE'] = env + # fetch all the non secret values + for v in self.envars: + value = v.get_value(env, account, fetch_pstore=True) + if value and not isinstance(value, Secret): + if v.name not in template_vars.keys(): + template_vars[v.name] = value + envars[v.name] = value - jenv = jinja2.Environment() + jenv = jinja2.Environment() - # process jinja templates - for var in envars: - envars[var] = jenv.from_string(envars[var]).render(template_vars) + # process jinja templates + if templating: + for var in envars: + envars[var] = jenv.from_string(envars[var]).render(template_vars) - # fetch secrets - for v in self.envars: - value = v.get_value(env, account, decrypt) - if value: - if v.name not in envars: + # fetch secrets + for v in self.envars: + value = v.get_value(env, account, decrypt) + if value: + if v.name not in envars: + envars[v.name] = value + + elif secrets_only: + for v in self.envars: + if isinstance(v.get_value(env, account), Secret): + envars[v.name] = v.get_value(env, account, decrypt) + else: + for v in self.envars: + value = v.get_value(env, account, decrypt) + if value: envars[v.name] = value return envars diff --git a/setup.py b/setup.py index 055b607..bba61ca 100644 --- a/setup.py +++ b/setup.py @@ -1,9 +1,10 @@ from setuptools import setup setup( - name='envars', - version='0.1', + name='timeout-envars', + version='0.3', description='A sample Python package', + long_description='my test long description', author='Keith Harvey', author_email='keith.harvey@timeout.com', packages=['envars'], diff --git a/tests/test_cli.py b/tests/test_cli.py index 27ce04e..4316722 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -97,16 +97,12 @@ def test_eqauls_in_value(tmp_path): }) envars.add_var(args) - args = type('Args', (object,), { - 'filename': f'{tmp_path}/envars.yml', - 'env': 'prod', - 'account': None, - 'template_var': [], - 'yaml': False, - 'decrypt': True, - 'quote': False - }) - ret = envars.process(args) + ret = envars.process( + filename=f'{tmp_path}/envars.yml', + account=None, + env='prod', + decrypt=True, + ) assert ret == ['TEST1=abc='] @@ -132,16 +128,12 @@ def test_two_env_vars_returned(tmp_path): }) envars.add_var(args) - args = type('Args', (object,), { - 'filename': f'{tmp_path}/envars.yml', - 'env': 'prod', - 'account': None, - 'template_var': [], - 'yaml': False, - 'decrypt': True, - 'quote': False - }) - ret = envars.process(args) + ret = envars.process( + filename=f'{tmp_path}/envars.yml', + account=None, + env='prod', + decrypt=True, + ) assert ret == ['TEST1=A', 'TEST2=B'] @@ -167,16 +159,12 @@ def test_template_var(tmp_path): }) envars.add_var(args) - args = type('Args', (object,), { - 'filename': f'{tmp_path}/envars.yml', - 'env': 'prod', - 'account': None, - 'template_var': [], - 'yaml': False, - 'decrypt': True, - 'quote': False - }) - ret = envars.process(args) + ret = envars.process( + filename=f'{tmp_path}/envars.yml', + account=None, + env='prod', + decrypt=True, + ) assert ret == ['DOMAIN=timeout.com', 'HOSTNAME=test.timeout.com'] @@ -193,16 +181,13 @@ def test_extra_template_passing(tmp_path): }) envars.add_var(args) - args = type('Args', (object,), { - 'filename': f'{tmp_path}/envars.yml', - 'env': 'prod', - 'account': None, - 'yaml': False, - 'decrypt': True, - 'template_var': ['RELEASE=12324523523523525234523523'], - 'quote': False - }) - ret = envars.process(args) + ret = envars.process( + filename=f'{tmp_path}/envars.yml', + account=None, + env='prod', + decrypt=True, + template_var=['RELEASE=12324523523523525234523523'], + ) assert ret == ['RELEASE=12324523523523525234523523'] @@ -238,17 +223,14 @@ def test_yaml_print_env(tmp_path): }) envars.add_var(args) - args = type('Args', (object,), { - 'filename': f'{tmp_path}/envars.yml', - 'env': 'prod', - 'var': None, - 'account': None, - 'yaml': True, - 'decrypt': True, - 'template_var': ['RELEASE=12324523523523525234523523'], - 'quote': False - }) - ret = envars.process(args) + ret = envars.process( + filename=f'{tmp_path}/envars.yml', + account=None, + env='prod', + decrypt=True, + as_yaml=True, + template_var=['RELEASE=12324523523523525234523523'], + ) assert ret == "envars:\n RELEASE: '12324523523523525234523523'\n TEST: test\n" @@ -272,16 +254,13 @@ def test_secret(kms_stub, tmp_path): 'account': None, }) envars.add_var(args) - args = type('Arg', (object,), { - 'filename': f'{tmp_path}/envars.yml', - 'env': 'prod', - 'account': None, - 'template_var': [], - 'yaml': False, - 'decrypt': True, - 'quote': False - }) - ret = envars.process(args) + + ret = envars.process( + filename=f'{tmp_path}/envars.yml', + account=None, + env='prod', + decrypt=True, + ) assert ret == ['TEST=sssssh'] @@ -302,16 +281,14 @@ def test_parameter_store_value(ssm_stub, tmp_path): }) envars.add_var(args) - args = type('Arg', (object,), { - 'filename': f'{tmp_path}/envars.yml', - 'env': 'prod', - 'account': None, - 'template_var': ['RELEASE=1234'], - 'yaml': False, - 'decrypt': False, - 'quote': False - }) - ret = envars.process(args) + ret = envars.process( + filename=f'{tmp_path}/envars.yml', + account=None, + env='prod', + decrypt=True, + template_var=['RELEASE=1234'], + ) + assert ret == ['PTEST=1234'] @@ -343,7 +320,8 @@ def test_exec_one_var(tmp_path): 'env': 'prod', 'filename': f'{tmp_path}/envars.yml', 'var': 'TEST', - 'quote': False + 'quote': False, + 'no_templating': False, }) envars.os.execlp = MagicMock() envars.execute(args) @@ -381,10 +359,92 @@ def test_exec(tmp_path): 'filename': f'{tmp_path}/envars.yml', 'var': None, 'template_var': [], - 'quote': False + 'quote': False, + 'no_templating': False, }) envars.os.execlp = MagicMock() envars.execute(args) assert os.environ.get('TEST') == 'test' assert os.environ.get('STEST') == 'stest=' + + +def test_secrets_only(kms_stub, tmp_path): + kms_stub.add_response( + 'encrypt', + service_response={'CiphertextBlob': b'dfghsdghfsd'} + ) + kms_stub.add_response( + 'decrypt', + service_response={'KeyId': 'STEST', 'Plaintext': b'stest', 'EncryptionAlgorithm': 'SYMMETRIC_DEFAULT'} + ) + run_cmd(tmp_path, 'init --app testapp --environments prod,staging --kms-key-arn abc') + args = type('Args', (object,), { + 'variable': 'TEST=test', + 'secret': False, + 'filename': f'{tmp_path}/envars.yml', + 'env': 'default', + 'desc': None, + 'account': None, + }) + envars.add_var(args) + args = type('Args', (object,), { + 'variable': 'STEST=stest', + 'secret': True, + 'filename': f'{tmp_path}/envars.yml', + 'env': 'default', + 'desc': None, + 'account': None, + }) + envars.add_var(args) + + ret = envars.process( + filename=f'{tmp_path}/envars.yml', + account=None, + env='prod', + decrypt=True, + secrets_only=True, + ) + + assert ret == ['STEST=stest'] + + +def test_secrets_only_yaml(kms_stub, tmp_path): + kms_stub.add_response( + 'encrypt', + service_response={'CiphertextBlob': b'dfghsdghfsd'} + ) + kms_stub.add_response( + 'decrypt', + service_response={'KeyId': 'STEST', 'Plaintext': b'stest', 'EncryptionAlgorithm': 'SYMMETRIC_DEFAULT'} + ) + run_cmd(tmp_path, 'init --app testapp --environments prod,staging --kms-key-arn abc') + args = type('Args', (object,), { + 'variable': 'TEST=test', + 'secret': False, + 'filename': f'{tmp_path}/envars.yml', + 'env': 'default', + 'desc': None, + 'account': None, + }) + envars.add_var(args) + args = type('Args', (object,), { + 'variable': 'STEST=stest', + 'secret': True, + 'filename': f'{tmp_path}/envars.yml', + 'env': 'default', + 'desc': None, + 'account': None, + }) + envars.add_var(args) + + ret = envars.process( + filename=f'{tmp_path}/envars.yml', + account=None, + env='prod', + decrypt=True, + secrets_only=True, + as_yaml=True, + ) + + assert ret == 'envars:\n STEST: stest\n'