Merge pull request #696 from jladdjr/awx_349_custom_cred_write_multiple_files

Feature: Multi-file support for Credential Types
This commit is contained in:
Ryan Petrello
2018-02-02 11:39:11 -05:00
committed by GitHub
5 changed files with 84 additions and 13 deletions

View File

@@ -695,11 +695,10 @@ class CredentialTypeInjectorField(JSONSchemaField):
'properties': { 'properties': {
'file': { 'file': {
'type': 'object', 'type': 'object',
'properties': { 'patternProperties': {
'template': {'type': 'string'}, '^template(\.[a-zA-Z_]+[a-zA-Z0-9_]*)?$': {'type': 'string'},
}, },
'additionalProperties': False, 'additionalProperties': False,
'required': ['template'],
}, },
'env': { 'env': {
'type': 'object', 'type': 'object',
@@ -749,8 +748,22 @@ class CredentialTypeInjectorField(JSONSchemaField):
class TowerNamespace: class TowerNamespace:
filename = None filename = None
valid_namespace['tower'] = TowerNamespace() valid_namespace['tower'] = TowerNamespace()
# ensure either single file or multi-file syntax is used (but not both)
template_names = [x for x in value.get('file', {}).keys() if x.startswith('template')]
if 'template' in template_names and len(template_names) > 1:
raise django_exceptions.ValidationError(
_('Must use multi-file syntax when injecting multiple files'),
code='invalid',
params={'value': value},
)
if 'template' not in template_names:
valid_namespace['tower'].filename = TowerNamespace()
for template_name in template_names:
template_name = template_name.split('.')[1]
setattr(valid_namespace['tower'].filename, template_name, 'EXAMPLE')
for type_, injector in value.items(): for type_, injector in value.items():
for key, tmpl in injector.items(): for key, tmpl in injector.items():
try: try:

View File

@@ -594,7 +594,7 @@ class CredentialType(CommonModelNameNotUnique):
return return
class TowerNamespace: class TowerNamespace:
filename = None pass
tower_namespace = TowerNamespace() tower_namespace = TowerNamespace()
@@ -622,17 +622,25 @@ class CredentialType(CommonModelNameNotUnique):
if len(value): if len(value):
namespace[field_name] = value namespace[field_name] = value
file_tmpl = self.injectors.get('file', {}).get('template') file_tmpls = self.injectors.get('file', {})
if file_tmpl is not None: # If any file templates are provided, render the files and update the
# If a file template is provided, render the file and update the # special `tower` template namespace so the filename can be
# special `tower` template namespace so the filename can be # referenced in other injectors
# referenced in other injectors for file_label, file_tmpl in file_tmpls.items():
data = Template(file_tmpl).render(**namespace) data = Template(file_tmpl).render(**namespace)
_, path = tempfile.mkstemp(dir=private_data_dir) _, path = tempfile.mkstemp(dir=private_data_dir)
with open(path, 'w') as f: with open(path, 'w') as f:
f.write(data) f.write(data)
os.chmod(path, stat.S_IRUSR | stat.S_IWUSR) os.chmod(path, stat.S_IRUSR | stat.S_IWUSR)
namespace['tower'].filename = path
# determine if filename indicates single file or many
if file_label.find('.') == -1:
tower_namespace.filename = path
else:
if not hasattr(tower_namespace, 'filename'):
tower_namespace.filename = TowerNamespace()
file_label = file_label.split('.')[1]
setattr(tower_namespace.filename, file_label, path)
for env_var, tmpl in self.injectors.get('env', {}).items(): for env_var, tmpl in self.injectors.get('env', {}).items():
if env_var.startswith('ANSIBLE_') or env_var in self.ENV_BLACKLIST: if env_var.startswith('ANSIBLE_') or env_var in self.ENV_BLACKLIST:

View File

@@ -107,8 +107,11 @@ def test_cred_type_input_schema_validity(input_, valid):
({}, True), ({}, True),
({'invalid-injector': {}}, False), ({'invalid-injector': {}}, False),
({'file': 123}, False), ({'file': 123}, False),
({'file': {}}, False), ({'file': {}}, True),
({'file': {'template': '{{username}}'}}, True), ({'file': {'template': '{{username}}'}}, True),
({'file': {'template.username': '{{username}}'}}, True),
({'file': {'template.username': '{{username}}', 'template.password': '{{pass}}'}}, True),
({'file': {'template': '{{username}}', 'template.password': '{{pass}}'}}, False),
({'file': {'foo': 'bar'}}, False), ({'file': {'foo': 'bar'}}, False),
({'env': 123}, False), ({'env': 123}, False),
({'env': {}}, True), ({'env': {}}, True),

View File

@@ -1227,6 +1227,50 @@ class TestJobCredentials(TestJobExecution):
self.run_pexpect.side_effect = run_pexpect_side_effect self.run_pexpect.side_effect = run_pexpect_side_effect
self.task.run(self.pk) self.task.run(self.pk)
def test_custom_environment_injectors_with_files(self):
some_cloud = CredentialType(
kind='cloud',
name='SomeCloud',
managed_by_tower=False,
inputs={
'fields': [{
'id': 'cert',
'label': 'Certificate',
'type': 'string'
}, {
'id': 'key',
'label': 'Key',
'type': 'string'
}]
},
injectors={
'file': {
'template.cert': '[mycert]\n{{cert}}',
'template.key': '[mykey]\n{{key}}'
},
'env': {
'MY_CERT_INI_FILE': '{{tower.filename.cert}}',
'MY_KEY_INI_FILE': '{{tower.filename.key}}'
}
}
)
credential = Credential(
pk=1,
credential_type=some_cloud,
inputs = {'cert': 'CERT123', 'key': 'KEY123'}
)
self.instance.credentials.add(credential)
self.task.run(self.pk)
def run_pexpect_side_effect(*args, **kwargs):
args, cwd, env, stdout = args
assert open(env['MY_CERT_INI_FILE'], 'rb').read() == '[mycert]\nCERT123'
assert open(env['MY_KEY_INI_FILE'], 'rb').read() == '[mykey]\nKEY123'
return ['successful', 0]
self.run_pexpect.side_effect = run_pexpect_side_effect
self.task.run(self.pk)
def test_multi_cloud(self): def test_multi_cloud(self):
gce = CredentialType.defaults['gce']() gce = CredentialType.defaults['gce']()
gce_credential = Credential( gce_credential = Credential(

View File

@@ -194,7 +194,8 @@ certificate/key data:
} }
} }
Note that the single and multi-file syntax cannot be mixed within the same
``Credential Type``.
Job and Job Template Credential Assignment Job and Job Template Credential Assignment
------------------------------------------ ------------------------------------------
@@ -326,6 +327,8 @@ When verifying acceptance we should ensure the following statements are true:
* Custom `Credential Types` should support injecting both single and * Custom `Credential Types` should support injecting both single and
multiple files. (Furthermore, the new syntax for injecting multiple files multiple files. (Furthermore, the new syntax for injecting multiple files
should work properly even if only a single file is injected). should work properly even if only a single file is injected).
* Users should not be able to use the syntax for injecting single and
multiple files in the same custom credential.
* The default `Credential Types` included with Tower in 3.2 should be * The default `Credential Types` included with Tower in 3.2 should be
non-editable/readonly and cannot be deleted by any user. non-editable/readonly and cannot be deleted by any user.
* Stored `Credential` values for _all_ types should be consistent before and * Stored `Credential` values for _all_ types should be consistent before and