Skip to content

Commit 7086e5d

Browse files
committed
feat: SOPS secrets support
Signed-off-by: Aurora Gaffney <agaffney@applause.com>
1 parent e782304 commit 7086e5d

File tree

12 files changed

+129
-8
lines changed

12 files changed

+129
-8
lines changed

deploy_config_generator/__main__.py

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
from deploy_config_generator.template import Template
1717
from deploy_config_generator.errors import DeployConfigError, DeployConfigGenerationError, ConfigError, VarsReplacementError
1818
from deploy_config_generator.utils import yaml_dump, show_traceback
19+
from deploy_config_generator.secrets import Secrets, SecretsException
1920

2021
DISPLAY = None
2122
SITE_CONFIG = None
@@ -78,7 +79,7 @@ def load_vars_files(varset, vars_dir, patterns, allow_var_references=True):
7879
varset.read_vars_file(vars_file, allow_var_references=allow_var_references)
7980

8081

81-
def load_output_plugins(varset, output_dir, config_version):
82+
def load_output_plugins(varset, secrets, output_dir, config_version):
8283
'''
8384
Find, import, and instantiate all output plugins
8485
'''
@@ -111,7 +112,7 @@ def load_output_plugins(varset, output_dir, config_version):
111112
raise Exception('name specified in OutputPlugin class (%s) does not match file name (%s)' % (cls.NAME, name))
112113
DISPLAY.v('Loading plugin %s' % name)
113114
# Instantiate plugin class
114-
plugins.append(cls(varset, output_dir, config_version))
115+
plugins.append(cls(varset, secrets, output_dir, config_version))
115116
except ConfigError as e:
116117
DISPLAY.display('Plugin configuration error: %s: %s' % (name, str(e)))
117118
sys.exit(1)
@@ -263,8 +264,24 @@ def main():
263264
DISPLAY.vvvv()
264265
DISPLAY.vvvv(yaml_dump(dict(varset), indent=2))
265266

267+
secrets = Secrets()
268+
269+
try:
270+
for f in SITE_CONFIG.secrets_files:
271+
secrets.load_secrets_file(f)
272+
except SecretsException as e:
273+
DISPLAY.display('Error loading secrets file %s' % (e.path,))
274+
DISPLAY.v('Stderr:')
275+
DISPLAY.v()
276+
DISPLAY.v(e.stderr)
277+
sys.exit(1)
278+
279+
DISPLAY.vvvv('Secrets:')
280+
DISPLAY.vvvv()
281+
DISPLAY.vvvv(yaml_dump(dict(secrets), indent=2))
282+
266283
try:
267-
deploy_config = DeployConfig(os.path.join(deploy_dir, SITE_CONFIG.deploy_config_file), varset)
284+
deploy_config = DeployConfig(os.path.join(deploy_dir, SITE_CONFIG.deploy_config_file), varset, secrets)
268285
deploy_config.set_config(varset.replace_vars(deploy_config.get_config()))
269286
except DeployConfigError as e:
270287
DISPLAY.display('Error loading deploy config: %s' % str(e))
@@ -280,7 +297,7 @@ def main():
280297
DISPLAY.vvvv(yaml_dump(deploy_config.get_config(), indent=2))
281298

282299
deploy_config_version = deploy_config.get_version() or SITE_CONFIG.default_config_version
283-
output_plugins = load_output_plugins(varset, args.output_dir, deploy_config_version)
300+
output_plugins = load_output_plugins(varset, secrets, args.output_dir, deploy_config_version)
284301

285302
DISPLAY.vvv('Available output plugins:')
286303
DISPLAY.vvv()

deploy_config_generator/deploy_config.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,9 @@ class DeployConfig(object):
1212
_path = None
1313
_version = None
1414

15-
def __init__(self, path, varset):
15+
def __init__(self, path, varset, secrets):
1616
self._vars = varset
17+
self._secrets = secrets
1718
self._display = Display()
1819
self.load(path)
1920

@@ -68,7 +69,7 @@ def apply_default_apps(self, default_apps):
6869
condition = app[CONDITION_KEY]
6970
del app[CONDITION_KEY]
7071
template = Template()
71-
tmp_vars = dict(VARS=dict(self._vars))
72+
tmp_vars = dict(VARS=dict(self._vars), SECRETS=dict(self._secrets))
7273
# Continue to next item if we have a condition and it evaluated to False
7374
if not template.evaluate_condition(condition, tmp_vars):
7475
continue

deploy_config_generator/output/__init__.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,9 @@ class OutputPluginBase(object):
3030
)
3131
PRIORITY = 1
3232

33-
def __init__(self, varset, output_dir, config_version):
33+
def __init__(self, varset, secrets, output_dir, config_version):
3434
self._vars = varset
35+
self._secrets = secrets
3536
self._output_dir = output_dir
3637
self._display = Display()
3738
self._template = Template()
@@ -201,6 +202,8 @@ def build_app_vars(self, index, app, path=''):
201202
'APP': self.merge_with_field_defaults(app),
202203
# Parsed vars
203204
'VARS': dict(self._vars),
205+
# Secrets
206+
'SECRETS': dict(self._secrets),
204207
}
205208
return app_vars
206209

deploy_config_generator/secrets.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import subprocess
2+
3+
from deploy_config_generator.utils import yaml_load
4+
5+
SOPS_BINARY = 'sops'
6+
7+
8+
class SecretsException(Exception):
9+
10+
def __init__(self, path, stderr):
11+
self.path = path
12+
self.stderr = stderr
13+
super().__init__('Failed to decrypt secrets')
14+
15+
16+
class Secrets(dict):
17+
18+
'''
19+
This class manages a set of SOPS-encrypted secrets
20+
'''
21+
22+
def load_secrets_file(self, path):
23+
cp = subprocess.run([SOPS_BINARY, 'decrypt', '--output-type=yaml', path], capture_output=True)
24+
if cp.returncode != 0:
25+
raise SecretsException(path, cp.stderr)
26+
# Parse decrypted YAML from SOPS
27+
data = yaml_load(cp.stdout)
28+
self.update(data)

deploy_config_generator/site_config.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,8 @@ class SiteConfig(object, metaclass=Singleton):
4949
'default_apps': {},
5050
# Default vars
5151
'default_vars': {},
52+
# SOPS secrets files to load
53+
'secrets_files': [],
5254
}
5355

5456
def __init__(self, env=None):

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ def run(self):
5353

5454
setup(
5555
name='deploy-config-generator',
56-
version='2.29.0',
56+
version='2.30.0',
5757
url='https://github.com/ApplauseOSS/deploy-config-generator',
5858
license='MIT',
5959
description='Utility to generate service deploy configurations',

tests/integration/age-key.txt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
# NOTICE: this key file was created for use in the integration tests and should not be
2+
# used elsewhere
3+
#
4+
# created: 2025-11-04T20:43:20Z
5+
# public key: age1cnlgtr6w8p8phdxqevklllex0u7y97574xclnf0qv050emsstgpqv23h0q
6+
AGE-SECRET-KEY-1D4HH6NNHZZSRM0WD23PEWT9FTWR2708FNDV957WPTJ0CE0WDFG9QY3QUSY
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
test:
3+
dummy: True
4+
parent1:
5+
- child2_1: '{{ SECRETS.foo }}'
6+
child2_2: '{{ SECRETS.some }}'
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
Dummy output plugin
2+
3+
App config:
4+
5+
{
6+
"dummy": true,
7+
"parent1": [
8+
{
9+
"child2_1": "bar",
10+
"child2_2": "value",
11+
"parent2": []
12+
}
13+
]
14+
}

tests/integration/secrets/runme.sh

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
#!/bin/bash
2+
3+
TEST_DIR=$(dirname $0)
4+
cd ${TEST_DIR}
5+
6+
export PYTHONPATH=../../..
7+
8+
export SOPS_AGE_KEY_FILE=../age-key.txt
9+
10+
set -e
11+
12+
rm -rf tmp
13+
mkdir tmp
14+
15+
set -x
16+
17+
python3 -m deploy_config_generator -v -c site_config.yml -o tmp . $@
18+
19+
diff -BurN expected_output tmp

0 commit comments

Comments
 (0)