mirror of
https://github.com/ZwareBear/awx.git
synced 2026-03-20 07:43:35 -05:00
Merge pull request #13267 from philipsd6/feature/complex_extra_vars
Enable support for injecting complex extra vars
This commit is contained in:
@@ -788,7 +788,8 @@ class CredentialTypeInjectorField(JSONSchemaField):
|
|||||||
'type': 'object',
|
'type': 'object',
|
||||||
'patternProperties': {
|
'patternProperties': {
|
||||||
# http://docs.ansible.com/ansible/playbooks_variables.html#what-makes-a-valid-variable-name
|
# http://docs.ansible.com/ansible/playbooks_variables.html#what-makes-a-valid-variable-name
|
||||||
'^[a-zA-Z_]+[a-zA-Z0-9_]*$': {'type': 'string'},
|
# plus, add ability to template
|
||||||
|
r'^[a-zA-Z_\{\}]+[a-zA-Z0-9_\{\}]*$': {"anyOf": [{'type': 'string'}, {'type': 'array'}, {'$ref': '#/properties/extra_vars'}]}
|
||||||
},
|
},
|
||||||
'additionalProperties': False,
|
'additionalProperties': False,
|
||||||
},
|
},
|
||||||
@@ -855,27 +856,44 @@ class CredentialTypeInjectorField(JSONSchemaField):
|
|||||||
template_name = template_name.split('.')[1]
|
template_name = template_name.split('.')[1]
|
||||||
setattr(valid_namespace['tower'].filename, template_name, 'EXAMPLE_FILENAME')
|
setattr(valid_namespace['tower'].filename, template_name, 'EXAMPLE_FILENAME')
|
||||||
|
|
||||||
|
def validate_template_string(type_, key, tmpl):
|
||||||
|
try:
|
||||||
|
sandbox.ImmutableSandboxedEnvironment(undefined=StrictUndefined).from_string(tmpl).render(valid_namespace)
|
||||||
|
except UndefinedError as e:
|
||||||
|
raise django_exceptions.ValidationError(
|
||||||
|
_('{sub_key} uses an undefined field ({error_msg})').format(sub_key=key, error_msg=e),
|
||||||
|
code='invalid',
|
||||||
|
params={'value': value},
|
||||||
|
)
|
||||||
|
except SecurityError as e:
|
||||||
|
raise django_exceptions.ValidationError(_('Encountered unsafe code execution: {}').format(e))
|
||||||
|
except TemplateSyntaxError as e:
|
||||||
|
raise django_exceptions.ValidationError(
|
||||||
|
_('Syntax error rendering template for {sub_key} inside of {type} ({error_msg})').format(sub_key=key, type=type_, error_msg=e),
|
||||||
|
code='invalid',
|
||||||
|
params={'value': value},
|
||||||
|
)
|
||||||
|
|
||||||
|
def validate_extra_vars(key, node):
|
||||||
|
if isinstance(node, dict):
|
||||||
|
for k, v in node.items():
|
||||||
|
validate_template_string("extra_vars", 'a key' if key is None else key, k)
|
||||||
|
validate_extra_vars(k if key is None else "{key}.{k}".format(key=key, k=k), v)
|
||||||
|
elif isinstance(node, list):
|
||||||
|
for i, x in enumerate(node):
|
||||||
|
validate_extra_vars("{key}[{i}]".format(key=key, i=i), x)
|
||||||
|
else:
|
||||||
|
validate_template_string("extra_vars", key, node)
|
||||||
|
|
||||||
for type_, injector in value.items():
|
for type_, injector in value.items():
|
||||||
if type_ == 'env':
|
if type_ == 'env':
|
||||||
for key in injector.keys():
|
for key in injector.keys():
|
||||||
self.validate_env_var_allowed(key)
|
self.validate_env_var_allowed(key)
|
||||||
for key, tmpl in injector.items():
|
if type_ == 'extra_vars':
|
||||||
try:
|
validate_extra_vars(None, injector)
|
||||||
sandbox.ImmutableSandboxedEnvironment(undefined=StrictUndefined).from_string(tmpl).render(valid_namespace)
|
else:
|
||||||
except UndefinedError as e:
|
for key, tmpl in injector.items():
|
||||||
raise django_exceptions.ValidationError(
|
validate_template_string(type_, key, tmpl)
|
||||||
_('{sub_key} uses an undefined field ({error_msg})').format(sub_key=key, error_msg=e),
|
|
||||||
code='invalid',
|
|
||||||
params={'value': value},
|
|
||||||
)
|
|
||||||
except SecurityError as e:
|
|
||||||
raise django_exceptions.ValidationError(_('Encountered unsafe code execution: {}').format(e))
|
|
||||||
except TemplateSyntaxError as e:
|
|
||||||
raise django_exceptions.ValidationError(
|
|
||||||
_('Syntax error rendering template for {sub_key} inside of {type} ({error_msg})').format(sub_key=key, type=type_, error_msg=e),
|
|
||||||
code='invalid',
|
|
||||||
params={'value': value},
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class AskForField(models.BooleanField):
|
class AskForField(models.BooleanField):
|
||||||
|
|||||||
@@ -528,9 +528,13 @@ class CredentialType(CommonModelNameNotUnique):
|
|||||||
|
|
||||||
if 'INVENTORY_UPDATE_ID' not in env:
|
if 'INVENTORY_UPDATE_ID' not in env:
|
||||||
# awx-manage inventory_update does not support extra_vars via -e
|
# awx-manage inventory_update does not support extra_vars via -e
|
||||||
extra_vars = {}
|
def build_extra_vars(node):
|
||||||
for var_name, tmpl in self.injectors.get('extra_vars', {}).items():
|
if isinstance(node, dict):
|
||||||
extra_vars[var_name] = sandbox_env.from_string(tmpl).render(**namespace)
|
return {build_extra_vars(k): build_extra_vars(v) for k, v in node.items()}
|
||||||
|
elif isinstance(node, list):
|
||||||
|
return [build_extra_vars(x) for x in node]
|
||||||
|
else:
|
||||||
|
return sandbox_env.from_string(node).render(**namespace)
|
||||||
|
|
||||||
def build_extra_vars_file(vars, private_dir):
|
def build_extra_vars_file(vars, private_dir):
|
||||||
handle, path = tempfile.mkstemp(dir=os.path.join(private_dir, 'env'))
|
handle, path = tempfile.mkstemp(dir=os.path.join(private_dir, 'env'))
|
||||||
@@ -540,6 +544,7 @@ class CredentialType(CommonModelNameNotUnique):
|
|||||||
os.chmod(path, stat.S_IRUSR)
|
os.chmod(path, stat.S_IRUSR)
|
||||||
return path
|
return path
|
||||||
|
|
||||||
|
extra_vars = build_extra_vars(self.injectors.get('extra_vars', {}))
|
||||||
if extra_vars:
|
if extra_vars:
|
||||||
path = build_extra_vars_file(extra_vars, private_data_dir)
|
path = build_extra_vars_file(extra_vars, private_data_dir)
|
||||||
container_path = to_container_path(path, private_data_dir)
|
container_path = to_container_path(path, private_data_dir)
|
||||||
|
|||||||
@@ -1209,6 +1209,42 @@ class TestJobCredentials(TestJobExecution):
|
|||||||
assert extra_vars["turbo_button"] == "True"
|
assert extra_vars["turbo_button"] == "True"
|
||||||
return ['successful', 0]
|
return ['successful', 0]
|
||||||
|
|
||||||
|
def test_custom_environment_injectors_with_nested_extra_vars(self, private_data_dir, job, mock_me):
|
||||||
|
task = jobs.RunJob()
|
||||||
|
some_cloud = CredentialType(
|
||||||
|
kind='cloud',
|
||||||
|
name='SomeCloud',
|
||||||
|
managed=False,
|
||||||
|
inputs={'fields': [{'id': 'host', 'label': 'Host', 'type': 'string'}]},
|
||||||
|
injectors={'extra_vars': {'auth': {'host': '{{host}}'}}},
|
||||||
|
)
|
||||||
|
credential = Credential(pk=1, credential_type=some_cloud, inputs={'host': 'example.com'})
|
||||||
|
job.credentials.add(credential)
|
||||||
|
|
||||||
|
args = task.build_args(job, private_data_dir, {})
|
||||||
|
credential.credential_type.inject_credential(credential, {}, {}, args, private_data_dir)
|
||||||
|
extra_vars = parse_extra_vars(args, private_data_dir)
|
||||||
|
|
||||||
|
assert extra_vars["auth"]["host"] == "example.com"
|
||||||
|
|
||||||
|
def test_custom_environment_injectors_with_templated_extra_vars_key(self, private_data_dir, job, mock_me):
|
||||||
|
task = jobs.RunJob()
|
||||||
|
some_cloud = CredentialType(
|
||||||
|
kind='cloud',
|
||||||
|
name='SomeCloud',
|
||||||
|
managed=False,
|
||||||
|
inputs={'fields': [{'id': 'environment', 'label': 'Environment', 'type': 'string'}, {'id': 'host', 'label': 'Host', 'type': 'string'}]},
|
||||||
|
injectors={'extra_vars': {'{{environment}}_auth': {'host': '{{host}}'}}},
|
||||||
|
)
|
||||||
|
credential = Credential(pk=1, credential_type=some_cloud, inputs={'environment': 'test', 'host': 'example.com'})
|
||||||
|
job.credentials.add(credential)
|
||||||
|
|
||||||
|
args = task.build_args(job, private_data_dir, {})
|
||||||
|
credential.credential_type.inject_credential(credential, {}, {}, args, private_data_dir)
|
||||||
|
extra_vars = parse_extra_vars(args, private_data_dir)
|
||||||
|
|
||||||
|
assert extra_vars["test_auth"]["host"] == "example.com"
|
||||||
|
|
||||||
def test_custom_environment_injectors_with_complicated_boolean_template(self, job, private_data_dir, mock_me):
|
def test_custom_environment_injectors_with_complicated_boolean_template(self, job, private_data_dir, mock_me):
|
||||||
task = jobs.RunJob()
|
task = jobs.RunJob()
|
||||||
some_cloud = CredentialType(
|
some_cloud = CredentialType(
|
||||||
|
|||||||
@@ -172,7 +172,11 @@ of the [Jinja templating language](https://jinja.palletsprojects.com/en/2.10.x/)
|
|||||||
"THIRD_PARTY_CLOUD_API_TOKEN": "{{api_token}}"
|
"THIRD_PARTY_CLOUD_API_TOKEN": "{{api_token}}"
|
||||||
},
|
},
|
||||||
"extra_vars": {
|
"extra_vars": {
|
||||||
"some_extra_var": "{{username}}:{{password}"
|
"some_extra_var": "{{username}}:{{password}}",
|
||||||
|
"auth": {
|
||||||
|
"username": "{{username}}",
|
||||||
|
"password": "{{password}}"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user