adding prompt-to-launch field on Labels field in Workflow Templates; with necessary UI and testing changes

Co-authored-by: Keith Grant <keithjgrant@gmail.com>
This commit is contained in:
Sarabraj Singh 2022-08-03 14:27:35 -04:00 committed by Alan Rominger
parent 4e665ca77f
commit 663ef2cc64
No known key found for this signature in database
GPG Key ID: C2D7EAAA12B63559
24 changed files with 365 additions and 78 deletions

View File

@ -3199,7 +3199,7 @@ class JobRelaunchSerializer(BaseSerializer):
return attrs
class JobCreateScheduleSerializer(BaseSerializer):
class JobCreateScheduleSerializer(LabelsListMixin, BaseSerializer):
can_schedule = serializers.SerializerMethodField()
prompts = serializers.SerializerMethodField()
@ -3230,6 +3230,8 @@ class JobCreateScheduleSerializer(BaseSerializer):
if 'credentials' in ret:
all_creds = [self._summarize('credential', cred) for cred in ret['credentials']]
ret['credentials'] = all_creds
if 'labels' in ret:
ret['labels'] = self._summary_field_labels(obj)
return ret
except JobLaunchConfig.DoesNotExist:
return {'all': _('Unknown, job may have been ran before launch configurations were saved.')}
@ -3402,6 +3404,9 @@ class WorkflowJobTemplateSerializer(JobTemplateMixin, LabelsListMixin, UnifiedJo
limit = serializers.CharField(allow_blank=True, allow_null=True, required=False, default=None)
scm_branch = serializers.CharField(allow_blank=True, allow_null=True, required=False, default=None)
skip_tags = serializers.CharField(allow_blank=True, allow_null=True, required=False, default=None)
job_tags = serializers.CharField(allow_blank=True, allow_null=True, required=False, default=None)
class Meta:
model = WorkflowJobTemplate
fields = (
@ -3420,6 +3425,11 @@ class WorkflowJobTemplateSerializer(JobTemplateMixin, LabelsListMixin, UnifiedJo
'webhook_service',
'webhook_credential',
'-execution_environment',
'ask_labels_on_launch',
'ask_skip_tags_on_launch',
'ask_tags_on_launch',
'skip_tags',
'job_tags',
)
def get_related(self, obj):
@ -3458,12 +3468,13 @@ class WorkflowJobTemplateSerializer(JobTemplateMixin, LabelsListMixin, UnifiedJo
def validate_extra_vars(self, value):
return vars_validate_or_raise(value)
# posting
def validate(self, attrs):
attrs = super(WorkflowJobTemplateSerializer, self).validate(attrs)
# process char_prompts, these are not direct fields on the model
mock_obj = self.Meta.model()
for field_name in ('scm_branch', 'limit'):
for field_name in ('scm_branch', 'limit', 'skip_tags', 'job_tags'):
if field_name in attrs:
setattr(mock_obj, field_name, attrs[field_name])
attrs.pop(field_name)
@ -3489,6 +3500,9 @@ class WorkflowJobSerializer(LabelsListMixin, UnifiedJobSerializer):
limit = serializers.CharField(allow_blank=True, allow_null=True, required=False, default=None)
scm_branch = serializers.CharField(allow_blank=True, allow_null=True, required=False, default=None)
skip_tags = serializers.CharField(allow_blank=True, allow_null=True, required=False, default=None)
job_tags = serializers.CharField(allow_blank=True, allow_null=True, required=False, default=None)
class Meta:
model = WorkflowJob
fields = (
@ -3508,6 +3522,8 @@ class WorkflowJobSerializer(LabelsListMixin, UnifiedJobSerializer):
'webhook_service',
'webhook_credential',
'webhook_guid',
'skip_tags',
'job_tags',
)
def get_related(self, obj):
@ -4333,6 +4349,10 @@ class WorkflowJobLaunchSerializer(BaseSerializer):
scm_branch = serializers.CharField(required=False, write_only=True, allow_blank=True)
workflow_job_template_data = serializers.SerializerMethodField()
labels = serializers.PrimaryKeyRelatedField(many=True, queryset=Label.objects.all(), required=False, write_only=True)
skip_tags = serializers.CharField(required=False, write_only=True, allow_blank=True)
job_tags = serializers.CharField(required=False, write_only=True, allow_blank=True)
class Meta:
model = WorkflowJobTemplate
fields = (
@ -4352,8 +4372,22 @@ class WorkflowJobLaunchSerializer(BaseSerializer):
'workflow_job_template_data',
'survey_enabled',
'ask_variables_on_launch',
'ask_labels_on_launch',
'labels',
'ask_skip_tags_on_launch',
'ask_tags_on_launch',
'skip_tags',
'job_tags',
)
read_only_fields = (
'ask_inventory_on_launch',
'ask_variables_on_launch',
'ask_skip_tags_on_launch',
'ask_labels_on_launch',
'ask_limit_on_launch',
'ask_scm_branch_on_launch',
'ask_tags_on_launch',
)
read_only_fields = ('ask_inventory_on_launch', 'ask_variables_on_launch')
def get_survey_enabled(self, obj):
if obj:
@ -4361,10 +4395,15 @@ class WorkflowJobLaunchSerializer(BaseSerializer):
return False
def get_defaults(self, obj):
defaults_dict = {}
for field_name in WorkflowJobTemplate.get_ask_mapping().keys():
if field_name == 'inventory':
defaults_dict[field_name] = dict(name=getattrd(obj, '%s.name' % field_name, None), id=getattrd(obj, '%s.pk' % field_name, None))
elif field_name == 'labels':
for label in obj.labels.all():
label_dict = {"id": label.id, "name": label.name}
defaults_dict.setdefault(field_name, []).append(label_dict)
else:
defaults_dict[field_name] = getattr(obj, field_name)
return defaults_dict
@ -4373,6 +4412,7 @@ class WorkflowJobLaunchSerializer(BaseSerializer):
return dict(name=obj.name, id=obj.id, description=obj.description)
def validate(self, attrs):
template = self.instance
accepted, rejected, errors = template._accept_or_ignore_job_kwargs(**attrs)
@ -4390,6 +4430,7 @@ class WorkflowJobLaunchSerializer(BaseSerializer):
WFJT_inventory = template.inventory
WFJT_limit = template.limit
WFJT_scm_branch = template.scm_branch
super(WorkflowJobLaunchSerializer, self).validate(attrs)
template.extra_vars = WFJT_extra_vars
template.inventory = WFJT_inventory

View File

@ -3197,13 +3197,17 @@ class WorkflowJobTemplateLaunch(RetrieveAPIView):
data['extra_vars'] = extra_vars
modified_ask_mapping = models.WorkflowJobTemplate.get_ask_mapping()
modified_ask_mapping.pop('extra_vars')
for field_name, ask_field_name in obj.get_ask_mapping().items():
for field, ask_field_name in modified_ask_mapping.items():
if not getattr(obj, ask_field_name):
data.pop(field_name, None)
elif field_name == 'inventory':
data[field_name] = getattrd(obj, "%s.%s" % (field_name, 'id'), None)
data.pop(field, None)
elif isinstance(getattr(obj.__class__, field).field, ForeignKey):
data[field] = getattrd(obj, "%s.%s" % (field, 'id'), None)
elif isinstance(getattr(obj.__class__, field).field, ManyToManyField):
data[field] = [item.id for item in getattr(obj, field).all()]
else:
data[field_name] = getattr(obj, field_name)
data[field] = getattr(obj, field)
return data
def post(self, request, *args, **kwargs):

View File

@ -107,4 +107,20 @@ class Migration(migrations.Migration):
blank=True, editable=False, related_name='joblaunchconfigs', through='main.JobLaunchConfigInstanceGroupMembership', to='main.InstanceGroup'
),
),
# added WFJT prompts
migrations.AddField(
model_name='workflowjobtemplate',
name='ask_labels_on_launch',
field=awx.main.fields.AskForField(blank=True, default=False),
),
migrations.AddField(
model_name='workflowjobtemplate',
name='ask_skip_tags_on_launch',
field=awx.main.fields.AskForField(blank=True, default=False),
),
migrations.AddField(
model_name='workflowjobtemplate',
name='ask_tags_on_launch',
field=awx.main.fields.AskForField(blank=True, default=False),
),
]

View File

@ -227,15 +227,6 @@ class JobTemplate(UnifiedJobTemplate, JobOptions, SurveyJobTemplateMixin, Resour
blank=True,
default=False,
)
ask_limit_on_launch = AskForField(
blank=True,
default=False,
)
ask_tags_on_launch = AskForField(blank=True, default=False, allows_field='job_tags')
ask_skip_tags_on_launch = AskForField(
blank=True,
default=False,
)
ask_job_type_on_launch = AskForField(
blank=True,
default=False,
@ -244,20 +235,11 @@ class JobTemplate(UnifiedJobTemplate, JobOptions, SurveyJobTemplateMixin, Resour
blank=True,
default=False,
)
ask_inventory_on_launch = AskForField(
blank=True,
default=False,
)
ask_credential_on_launch = AskForField(blank=True, default=False, allows_field='credentials')
ask_scm_branch_on_launch = AskForField(blank=True, default=False, allows_field='scm_branch')
ask_execution_environment_on_launch = AskForField(
blank=True,
default=False,
)
ask_labels_on_launch = AskForField(
blank=True,
default=False,
)
ask_forks_on_launch = AskForField(
blank=True,
default=False,

View File

@ -104,6 +104,33 @@ class SurveyJobTemplateMixin(models.Model):
default=False,
)
survey_spec = prevent_search(JSONBlob(default=dict, blank=True))
ask_inventory_on_launch = AskForField(
blank=True,
default=False,
)
ask_limit_on_launch = AskForField(
blank=True,
default=False,
)
ask_scm_branch_on_launch = AskForField(
blank=True,
default=False,
allows_field='scm_branch',
)
ask_labels_on_launch = AskForField(
blank=True,
default=False,
)
ask_tags_on_launch = AskForField(
blank=True,
default=False,
allows_field='job_tags',
)
ask_skip_tags_on_launch = AskForField(
blank=True,
default=False,
)
ask_variables_on_launch = AskForField(blank=True, default=False, allows_field='extra_vars')
def survey_password_variables(self):

View File

@ -422,6 +422,7 @@ class UnifiedJobTemplate(PolymorphicModel, CommonModelNameNotUnique, ExecutionEn
if unified_job.__class__ in activity_stream_registrar.models:
activity_stream_create(None, unified_job, True)
unified_job.log_lifecycle("created")
return unified_job
@classmethod

View File

@ -29,7 +29,7 @@ from awx.main.models import prevent_search, accepts_json, UnifiedJobTemplate, Un
from awx.main.models.notifications import NotificationTemplate, JobNotificationMixin
from awx.main.models.base import CreatedModifiedModel, VarsDictProperty
from awx.main.models.rbac import ROLE_SINGLETON_SYSTEM_ADMINISTRATOR, ROLE_SINGLETON_SYSTEM_AUDITOR
from awx.main.fields import ImplicitRoleField, AskForField, JSONBlob
from awx.main.fields import ImplicitRoleField, JSONBlob
from awx.main.models.mixins import (
ResourceMixin,
SurveyJobTemplateMixin,
@ -385,7 +385,7 @@ class WorkflowJobOptions(LaunchTimeConfigBase):
@classmethod
def _get_unified_job_field_names(cls):
r = set(f.name for f in WorkflowJobOptions._meta.fields) | set(
['name', 'description', 'organization', 'survey_passwords', 'labels', 'limit', 'scm_branch']
['name', 'description', 'organization', 'survey_passwords', 'labels', 'limit', 'scm_branch', 'job_tags', 'skip_tags']
)
r.remove('char_prompts') # needed due to copying launch config to launch config
return r
@ -425,26 +425,28 @@ class WorkflowJobOptions(LaunchTimeConfigBase):
class WorkflowJobTemplate(UnifiedJobTemplate, WorkflowJobOptions, SurveyJobTemplateMixin, ResourceMixin, RelatedJobsMixin, WebhookTemplateMixin):
SOFT_UNIQUE_TOGETHER = [('polymorphic_ctype', 'name', 'organization')]
FIELDS_TO_PRESERVE_AT_COPY = ['labels', 'organization', 'instance_groups', 'workflow_job_template_nodes', 'credentials', 'survey_spec']
FIELDS_TO_PRESERVE_AT_COPY = [
'labels',
'organization',
'instance_groups',
'workflow_job_template_nodes',
'credentials',
'survey_spec',
'skip_tags',
'job_tags',
]
class Meta:
app_label = 'main'
ask_inventory_on_launch = AskForField(
notification_templates_approvals = models.ManyToManyField(
"NotificationTemplate",
blank=True,
default=False,
related_name='%(class)s_notification_templates_for_approvals',
)
ask_limit_on_launch = AskForField(
blank=True,
default=False,
admin_role = ImplicitRoleField(
parent_role=['singleton:' + ROLE_SINGLETON_SYSTEM_ADMINISTRATOR, 'organization.workflow_admin_role'],
)
ask_scm_branch_on_launch = AskForField(
blank=True,
default=False,
)
notification_templates_approvals = models.ManyToManyField("NotificationTemplate", blank=True, related_name='%(class)s_notification_templates_for_approvals')
admin_role = ImplicitRoleField(parent_role=['singleton:' + ROLE_SINGLETON_SYSTEM_ADMINISTRATOR, 'organization.workflow_admin_role'])
execute_role = ImplicitRoleField(
parent_role=[
'admin_role',

View File

@ -210,7 +210,7 @@ def mk_workflow_job_template(name, extra_vars='', spec=None, organization=None,
if extra_vars:
extra_vars = json.dumps(extra_vars)
wfjt = WorkflowJobTemplate(name=name, extra_vars=extra_vars, organization=organization, webhook_service=webhook_service)
wfjt = WorkflowJobTemplate.objects.create(name=name, extra_vars=extra_vars, organization=organization, webhook_service=webhook_service)
if spec:
wfjt.survey_spec = spec

View File

@ -706,7 +706,7 @@ def jt_linked(organization, project, inventory, machine_credential, credential,
@pytest.fixture
def workflow_job_template(organization):
wjt = WorkflowJobTemplate(name='test-workflow_job_template', organization=organization)
wjt = WorkflowJobTemplate.objects.create(name='test-workflow_job_template', organization=organization)
wjt.save()
return wjt

View File

@ -287,12 +287,25 @@ class TestWorkflowJobTemplatePrompts:
@pytest.fixture
def wfjt_prompts(self):
return WorkflowJobTemplate.objects.create(
ask_inventory_on_launch=True, ask_variables_on_launch=True, ask_limit_on_launch=True, ask_scm_branch_on_launch=True
ask_variables_on_launch=True,
ask_inventory_on_launch=True,
ask_tags_on_launch=True,
ask_labels_on_launch=True,
ask_limit_on_launch=True,
ask_scm_branch_on_launch=True,
ask_skip_tags_on_launch=True,
)
@pytest.fixture
def prompts_data(self, inventory):
return dict(inventory=inventory, extra_vars={'foo': 'bar'}, limit='webservers', scm_branch='release-3.3')
return dict(
inventory=inventory,
extra_vars={'foo': 'bar'},
limit='webservers',
scm_branch='release-3.3',
job_tags='foo',
skip_tags='bar',
)
def test_apply_workflow_job_prompts(self, workflow_job_template, wfjt_prompts, prompts_data, inventory):
# null or empty fields used
@ -300,6 +313,9 @@ class TestWorkflowJobTemplatePrompts:
assert workflow_job.limit is None
assert workflow_job.inventory is None
assert workflow_job.scm_branch is None
assert workflow_job.job_tags is None
assert workflow_job.skip_tags is None
assert len(workflow_job.labels.all()) is 0
# fields from prompts used
workflow_job = workflow_job_template.create_unified_job(**prompts_data)
@ -307,15 +323,21 @@ class TestWorkflowJobTemplatePrompts:
assert workflow_job.limit == 'webservers'
assert workflow_job.inventory == inventory
assert workflow_job.scm_branch == 'release-3.3'
assert workflow_job.job_tags == 'foo'
assert workflow_job.skip_tags == 'bar'
# non-null fields from WFJT used
workflow_job_template.inventory = inventory
workflow_job_template.limit = 'fooo'
workflow_job_template.scm_branch = 'bar'
workflow_job_template.job_tags = 'baz'
workflow_job_template.skip_tags = 'dinosaur'
workflow_job = workflow_job_template.create_unified_job()
assert workflow_job.limit == 'fooo'
assert workflow_job.inventory == inventory
assert workflow_job.scm_branch == 'bar'
assert workflow_job.job_tags == 'baz'
assert workflow_job.skip_tags == 'dinosaur'
@pytest.mark.django_db
def test_process_workflow_job_prompts(self, inventory, workflow_job_template, wfjt_prompts, prompts_data):
@ -340,12 +362,19 @@ class TestWorkflowJobTemplatePrompts:
ask_limit_on_launch=True,
scm_branch='bar',
ask_scm_branch_on_launch=True,
job_tags='foo',
skip_tags='bar',
),
user=org_admin,
expect=201,
)
wfjt = WorkflowJobTemplate.objects.get(id=r.data['id'])
assert wfjt.char_prompts == {'limit': 'foooo', 'scm_branch': 'bar'}
assert wfjt.char_prompts == {
'limit': 'foooo',
'scm_branch': 'bar',
'job_tags': 'foo',
'skip_tags': 'bar',
}
assert wfjt.ask_scm_branch_on_launch is True
assert wfjt.ask_limit_on_launch is True
@ -355,6 +384,67 @@ class TestWorkflowJobTemplatePrompts:
assert r.data['limit'] == 'prompt_limit'
assert r.data['scm_branch'] == 'prompt_branch'
@pytest.mark.django_db
def test_set_all_ask_for_prompts_false_from_post(self, post, organization, inventory, org_admin):
'''
Tests default behaviour and values of ask_for_* fields on WFJT via POST
'''
r = post(
url=reverse('api:workflow_job_template_list'),
data=dict(
name='workflow that tests ask_for prompts',
organization=organization.id,
inventory=inventory.id,
job_tags='',
skip_tags='',
),
user=org_admin,
expect=201,
)
wfjt = WorkflowJobTemplate.objects.get(id=r.data['id'])
assert wfjt.ask_inventory_on_launch is False
assert wfjt.ask_labels_on_launch is False
assert wfjt.ask_limit_on_launch is False
assert wfjt.ask_scm_branch_on_launch is False
assert wfjt.ask_skip_tags_on_launch is False
assert wfjt.ask_tags_on_launch is False
assert wfjt.ask_variables_on_launch is False
@pytest.mark.django_db
def test_set_all_ask_for_prompts_true_from_post(self, post, organization, inventory, org_admin):
'''
Tests behaviour and values of ask_for_* fields on WFJT via POST
'''
r = post(
url=reverse('api:workflow_job_template_list'),
data=dict(
name='workflow that tests ask_for prompts',
organization=organization.id,
inventory=inventory.id,
job_tags='',
skip_tags='',
ask_inventory_on_launch=True,
ask_labels_on_launch=True,
ask_limit_on_launch=True,
ask_scm_branch_on_launch=True,
ask_skip_tags_on_launch=True,
ask_tags_on_launch=True,
ask_variables_on_launch=True,
),
user=org_admin,
expect=201,
)
wfjt = WorkflowJobTemplate.objects.get(id=r.data['id'])
assert wfjt.ask_inventory_on_launch is True
assert wfjt.ask_labels_on_launch is True
assert wfjt.ask_limit_on_launch is True
assert wfjt.ask_scm_branch_on_launch is True
assert wfjt.ask_skip_tags_on_launch is True
assert wfjt.ask_tags_on_launch is True
assert wfjt.ask_variables_on_launch is True
@pytest.mark.django_db
def test_workflow_ancestors(organization):

View File

@ -11,6 +11,7 @@ from awx.api.serializers import (
from awx.main.models import Job, WorkflowJobTemplateNode, WorkflowJob, WorkflowJobNode, WorkflowJobTemplate, Project, Inventory, JobTemplate
@pytest.mark.django_db
@mock.patch('awx.api.serializers.UnifiedJobTemplateSerializer.get_related', lambda x, y: {})
class TestWorkflowJobTemplateSerializerGetRelated:
@pytest.fixture
@ -58,6 +59,7 @@ class TestWorkflowNodeBaseSerializerGetRelated:
assert 'unified_job_template' not in related
@pytest.mark.django_db
@mock.patch('awx.api.serializers.BaseSerializer.get_related', lambda x, y: {})
class TestWorkflowJobTemplateNodeSerializerGetRelated:
@pytest.fixture
@ -146,6 +148,7 @@ class TestWorkflowJobTemplateNodeSerializerCharPrompts:
assert WFJT_serializer.instance.limit == 'webservers'
@pytest.mark.django_db
@mock.patch('awx.api.serializers.BaseSerializer.validate', lambda self, attrs: attrs)
class TestWorkflowJobTemplateNodeSerializerSurveyPasswords:
@pytest.fixture
@ -162,7 +165,7 @@ class TestWorkflowJobTemplateNodeSerializerSurveyPasswords:
def test_set_survey_passwords_create(self, jt):
serializer = WorkflowJobTemplateNodeSerializer()
wfjt = WorkflowJobTemplate(name='fake-wfjt')
wfjt = WorkflowJobTemplate.objects.create(name='fake-wfjt')
attrs = serializer.validate({'unified_job_template': jt, 'workflow_job_template': wfjt, 'extra_data': {'var1': 'secret_answer'}})
assert 'survey_passwords' in attrs
assert 'var1' in attrs['survey_passwords']
@ -171,7 +174,7 @@ class TestWorkflowJobTemplateNodeSerializerSurveyPasswords:
def test_set_survey_passwords_modify(self, jt):
serializer = WorkflowJobTemplateNodeSerializer()
wfjt = WorkflowJobTemplate(name='fake-wfjt')
wfjt = WorkflowJobTemplate.objects.create(name='fake-wfjt')
serializer.instance = WorkflowJobTemplateNode(workflow_job_template=wfjt, unified_job_template=jt)
attrs = serializer.validate({'unified_job_template': jt, 'workflow_job_template': wfjt, 'extra_data': {'var1': 'secret_answer'}})
assert 'survey_passwords' in attrs
@ -181,7 +184,7 @@ class TestWorkflowJobTemplateNodeSerializerSurveyPasswords:
def test_use_db_answer(self, jt, mocker):
serializer = WorkflowJobTemplateNodeSerializer()
wfjt = WorkflowJobTemplate(name='fake-wfjt')
wfjt = WorkflowJobTemplate.objects.create(name='fake-wfjt')
serializer.instance = WorkflowJobTemplateNode(workflow_job_template=wfjt, unified_job_template=jt, extra_data={'var1': '$encrypted$foooooo'})
with mocker.patch('awx.main.models.mixins.decrypt_value', return_value='foo'):
attrs = serializer.validate({'unified_job_template': jt, 'workflow_job_template': wfjt, 'extra_data': {'var1': '$encrypted$'}})
@ -196,7 +199,7 @@ class TestWorkflowJobTemplateNodeSerializerSurveyPasswords:
with that particular var omitted so on launch time the default takes effect
"""
serializer = WorkflowJobTemplateNodeSerializer()
wfjt = WorkflowJobTemplate(name='fake-wfjt')
wfjt = WorkflowJobTemplate.objects.create(name='fake-wfjt')
jt.survey_spec['spec'][0]['default'] = '$encrypted$bar'
attrs = serializer.validate({'unified_job_template': jt, 'workflow_job_template': wfjt, 'extra_data': {'var1': '$encrypted$'}})
assert 'survey_passwords' in attrs

View File

@ -259,13 +259,14 @@ def test_survey_encryption_defaults(survey_spec_factory, question_type, default,
@pytest.mark.survey
@pytest.mark.django_db
class TestWorkflowSurveys:
def test_update_kwargs_survey_defaults(self, survey_spec_factory):
"Assure that the survey default over-rides a JT variable"
spec = survey_spec_factory('var1')
spec['spec'][0]['default'] = 3
spec['spec'][0]['required'] = False
wfjt = WorkflowJobTemplate(name="test-wfjt", survey_spec=spec, survey_enabled=True, extra_vars="var1: 5")
wfjt = WorkflowJobTemplate.objects.create(name="test-wfjt", survey_spec=spec, survey_enabled=True, extra_vars="var1: 5")
updated_extra_vars = wfjt._update_unified_job_kwargs({}, {})
assert 'extra_vars' in updated_extra_vars
assert json.loads(updated_extra_vars['extra_vars'])['var1'] == 3
@ -277,7 +278,7 @@ class TestWorkflowSurveys:
spec['spec'][0]['required'] = False
spec['spec'][1]['required'] = True
spec['spec'][2]['required'] = False
wfjt = WorkflowJobTemplate(name="test-wfjt", survey_spec=spec, survey_enabled=True, extra_vars="question2: hiworld")
wfjt = WorkflowJobTemplate.objects.create(name="test-wfjt", survey_spec=spec, survey_enabled=True, extra_vars="question2: hiworld")
assert wfjt.variables_needed_to_start == ['question2']
assert not wfjt.can_start_without_user_input()
@ -311,6 +312,6 @@ class TestExtraVarsNoPrompt:
self.process_vars_and_assert(jt, provided_vars, valid)
def test_wfjt_extra_vars_counting(self, provided_vars, valid):
wfjt = WorkflowJobTemplate(name='foo', extra_vars={'tmpl_var': 'bar'})
wfjt = WorkflowJobTemplate.objects.create(name='foo', extra_vars={'tmpl_var': 'bar'})
prompted_fields, ignored_fields, errors = wfjt._accept_or_ignore_job_kwargs(extra_vars=provided_vars)
self.process_vars_and_assert(wfjt, provided_vars, valid)

View File

@ -94,7 +94,7 @@ def workflow_job_unit():
@pytest.fixture
def workflow_job_template_unit():
return WorkflowJobTemplate(name='workflow')
return WorkflowJobTemplate.objects.create(name='workflow')
@pytest.fixture
@ -151,6 +151,7 @@ def test_node_getter_and_setters():
assert node.job_type == 'check'
@pytest.mark.django_db
class TestWorkflowJobCreate:
def test_create_no_prompts(self, wfjt_node_no_prompts, workflow_job_unit, mocker):
mock_create = mocker.MagicMock()
@ -183,6 +184,7 @@ class TestWorkflowJobCreate:
)
@pytest.mark.django_db
@mock.patch('awx.main.models.workflow.WorkflowNodeBase.get_parent_nodes', lambda self: [])
class TestWorkflowJobNodeJobKWARGS:
"""
@ -231,4 +233,12 @@ class TestWorkflowJobNodeJobKWARGS:
def test_get_ask_mapping_integrity():
assert list(WorkflowJobTemplate.get_ask_mapping().keys()) == ['extra_vars', 'inventory', 'limit', 'scm_branch']
assert list(WorkflowJobTemplate.get_ask_mapping().keys()) == [
'inventory',
'limit',
'scm_branch',
'labels',
'job_tags',
'skip_tags',
'extra_vars',
]

View File

@ -196,6 +196,7 @@ def test_jt_can_add_bad_data(user_unit):
assert not access.can_add({'asdf': 'asdf'})
@pytest.mark.django_db
class TestWorkflowAccessMethods:
@pytest.fixture
def workflow(self, workflow_job_template_factory):

View File

@ -16,8 +16,12 @@ import CredentialsStep from './steps/CredentialsStep';
import CredentialPasswordsStep from './steps/CredentialPasswordsStep';
import OtherPromptsStep from './steps/OtherPromptsStep';
import PreviewStep from './steps/PreviewStep';
import executionEnvironmentHelpTextStrings from 'screens/ExecutionEnvironment/shared/ExecutionEnvironment.helptext';
import { ExecutionEnvironment } from 'types';
import ExecutionEnvironmentStep from './steps/ExecutionEnvironmentStep';
jest.mock('../../api/models/Inventories');
jest.mock('../../api/models/ExecutionEnvironments');
jest.mock('../../api/models/CredentialTypes');
jest.mock('../../api/models/Credentials');
jest.mock('../../api/models/JobTemplates');
@ -150,13 +154,14 @@ describe('LaunchPrompt', () => {
const wizard = await waitForElement(wrapper, 'Wizard');
const steps = wizard.prop('steps');
expect(steps).toHaveLength(6);
expect(steps).toHaveLength(7);
expect(steps[0].name.props.children).toEqual('Inventory');
expect(steps[1].name.props.children).toEqual('Credentials');
expect(steps[2].name.props.children).toEqual('Credential passwords');
expect(steps[3].name.props.children).toEqual('Other prompts');
expect(steps[4].name.props.children).toEqual('Survey');
expect(steps[5].name.props.children).toEqual('Preview');
expect(steps[3].name.props.children).toEqual('Execution Environment');
expect(steps[4].name.props.children).toEqual('Other prompts');
expect(steps[5].name.props.children).toEqual('Survey');
expect(steps[6].name.props.children).toEqual('Preview');
expect(wizard.find('WizardHeader').prop('title')).toBe('Launch | Foobar');
expect(wizard.find('WizardHeader').prop('description')).toBe(
'Foo Description'

View File

@ -22,12 +22,18 @@ const jobTemplateData = {
allow_simultaneous: false,
ask_credential_on_launch: false,
ask_diff_mode_on_launch: false,
ask_execution_environment_on_launch: false,
ask_forks_on_launch: false,
ask_instance_groups_on_launch: false,
ask_inventory_on_launch: false,
ask_job_slice_count_on_launch: false,
ask_job_type_on_launch: false,
ask_labels_on_launch: false,
ask_limit_on_launch: false,
ask_scm_branch_on_launch: false,
ask_skip_tags_on_launch: false,
ask_tags_on_launch: false,
ask_timeout_on_launch: false,
ask_variables_on_launch: false,
ask_verbosity_on_launch: false,
ask_execution_environment_on_launch: false,

View File

@ -35,13 +35,18 @@ const mockJobTemplate = {
allow_simultaneous: false,
ask_scm_branch_on_launch: false,
ask_diff_mode_on_launch: false,
ask_execution_environment_on_launch: false,
ask_forks_on_launch: false,
ask_instance_groups_on_launch: false,
ask_variables_on_launch: false,
ask_limit_on_launch: false,
ask_tags_on_launch: false,
ask_skip_tags_on_launch: false,
ask_job_type_on_launch: false,
ask_labels_on_launch: false,
ask_verbosity_on_launch: false,
ask_inventory_on_launch: false,
ask_job_slice_count_on_launch: false,
ask_credential_on_launch: false,
ask_execution_environment_on_launch: false,
ask_forks_on_launch: false,

View File

@ -82,7 +82,7 @@ describe('<WorkflowJobTemplateAdd/>', () => {
test('calls workflowJobTemplatesAPI with correct information on submit', async () => {
await act(async () => {
wrapper.find('input#wfjt-name').simulate('change', {
target: { value: 'Alex', name: 'name' },
target: { value: 'Alex Singh', name: 'name' },
});
wrapper.find('LabelSelect').find('SelectToggle').simulate('click');
@ -104,18 +104,23 @@ describe('<WorkflowJobTemplateAdd/>', () => {
wrapper.find('form').simulate('submit');
});
await expect(WorkflowJobTemplatesAPI.create).toHaveBeenCalledWith({
name: 'Alex',
name: 'Alex Singh',
allow_simultaneous: false,
ask_inventory_on_launch: false,
ask_labels_on_launch: false,
ask_limit_on_launch: false,
ask_scm_branch_on_launch: false,
ask_skip_tags_on_launch: false,
ask_tags_on_launch: false,
ask_variables_on_launch: false,
description: '',
extra_vars: '---',
inventory: undefined,
job_tags: '',
limit: null,
organization: undefined,
scm_branch: '',
skip_tags: '',
webhook_credential: undefined,
webhook_service: '',
webhook_url: '',

View File

@ -161,6 +161,7 @@ describe('<WorkflowJobTemplateEdit/>', () => {
expect(WorkflowJobTemplatesAPI.update).toHaveBeenCalledWith(6, {
name: 'Alex',
description: 'Apollo and Athena',
skip_tags: '',
inventory: 1,
organization: 1,
scm_branch: 'main',
@ -174,6 +175,11 @@ describe('<WorkflowJobTemplateEdit/>', () => {
ask_limit_on_launch: false,
ask_scm_branch_on_launch: false,
ask_variables_on_launch: false,
ask_labels_on_launch: false,
ask_skip_tags_on_launch: false,
ask_tags_on_launch: false,
job_tags: '',
skip_tags: '',
});
wrapper.update();
await expect(WorkflowJobTemplatesAPI.disassociateLabel).toBeCalledWith(6, {
@ -273,16 +279,21 @@ describe('<WorkflowJobTemplateEdit/>', () => {
expect(WorkflowJobTemplatesAPI.update).toBeCalledWith(6, {
allow_simultaneous: false,
ask_inventory_on_launch: false,
ask_labels_on_launch: false,
ask_limit_on_launch: false,
ask_scm_branch_on_launch: false,
ask_skip_tags_on_launch: false,
ask_tags_on_launch: false,
ask_variables_on_launch: false,
description: 'bar',
extra_vars: '---',
inventory: 1,
job_tags: '',
limit: '5000',
name: 'Foo',
organization: 1,
scm_branch: 'devel',
skip_tags: '',
webhook_credential: null,
webhook_service: '',
webhook_url: '',

View File

@ -18,6 +18,7 @@ const wfHelpTextStrings = () => ({
webhookKey: t`Webhook services can use this as a shared secret.`,
webhookCredential: t`Optionally select the credential to use to send status updates back to the webhook service.`,
webhookService: t`Select a webhook service.`,
skipTags: t`Skip tags are useful when you have a large playbook, and you want to skip specific parts of a play or task. Use commas to separate multiple tags. Refer to the documentation for details on the usage of tags.`,
enabledOptions: (
<>
<p>{t`Concurrent jobs: If enabled, simultaneous runs of this workflow job template will be allowed.`}</p>

View File

@ -27,6 +27,7 @@ import CheckboxField from 'components/FormField/CheckboxField';
import Popover from 'components/Popover';
import { WorkFlowJobTemplate } from 'types';
import LabelSelect from 'components/LabelSelect';
import { TagMultiSelect } from 'components/MultiSelect';
import WebhookSubForm from './WebhookSubForm';
import getHelpText from './WorkflowJobTemplate.helptext';
@ -59,6 +60,8 @@ function WorkflowJobTemplateForm({
const [, webhookKeyMeta, webhookKeyHelpers] = useField('webhook_key');
const [, webhookCredentialMeta, webhookCredentialHelpers] =
useField('webhook_credential');
const [skipTagsField, , skipTagsHelpers] = useField('skip_tags');
const [jobTagsField, , jobTagsHelpers] = useField('job_tags');
useEffect(() => {
if (enableWebhooks) {
@ -167,7 +170,6 @@ function WorkflowJobTemplateForm({
}}
/>
</FieldWithPrompt>
<FieldWithPrompt
fieldId="wfjt-scm-branch"
label={t`Source control branch`}
@ -184,14 +186,11 @@ function WorkflowJobTemplateForm({
aria-label={t`source control branch`}
/>
</FieldWithPrompt>
</FormColumnLayout>
<FormFullWidthLayout>
<FieldWithPrompt
fieldId="template-labels"
label={t`Labels`}
fieldId="template-labels"
promptId="template-ask-labels-on-launch"
promptName="ask_labels_on_launch"
tooltip={helpText.labels}
>
<LabelSelect
value={labelsField.value}
@ -200,16 +199,42 @@ function WorkflowJobTemplateForm({
createText={t`Create`}
/>
</FieldWithPrompt>
</FormFullWidthLayout>
<FormFullWidthLayout>
<VariablesField
id="wfjt-variables"
name="extra_vars"
label={t`Variables`}
promptId="template-ask-variables-on-launch"
tooltip={helpText.variables}
/>
</FormFullWidthLayout>
<FormFullWidthLayout>
<VariablesField
id="wfjt-variables"
name="extra_vars"
label={t`Variables`}
promptId="template-ask-variables-on-launch"
tooltip={helpText.variables}
/>
</FormFullWidthLayout>
<FormColumnLayout>
<FieldWithPrompt
fieldId="template-tags"
label={t`Job Tags`}
promptId="template-ask-tags-on-launch"
promptName="ask_tags_on_launch"
tooltip={helpText.jobTags}
>
<TagMultiSelect
value={jobTagsField.value}
onChange={(value) => jobTagsHelpers.setValue(value)}
/>
</FieldWithPrompt>
</FormColumnLayout>
<FieldWithPrompt
fieldId="template-skip-tags"
label={t`Skip Tags`}
promptId="template-ask-skip-tags-on-launch"
promptName="ask_skip_tags_on_launch"
tooltip={helpText.skipTags}
>
<TagMultiSelect
value={skipTagsField.value}
onChange={(value) => skipTagsHelpers.setValue(value)}
/>
</FieldWithPrompt>
</FormColumnLayout>
<FormGroup fieldId="options" label={t`Options`}>
<FormCheckboxLayout isInline>
<Checkbox
@ -282,6 +307,8 @@ const FormikApp = withFormik({
extra_vars: template.extra_vars || '---',
limit: template.limit || '',
scm_branch: template.scm_branch || '',
skip_tags: template.skip_tags || '',
job_tags: template.job_tags || '',
allow_simultaneous: template.allow_simultaneous || false,
webhook_credential: template?.summary_fields?.webhook_credential || null,
webhook_service: template.webhook_service || '',
@ -290,6 +317,8 @@ const FormikApp = withFormik({
ask_inventory_on_launch: template.ask_inventory_on_launch || false,
ask_variables_on_launch: template.ask_variables_on_launch || false,
ask_scm_branch_on_launch: template.ask_scm_branch_on_launch || false,
ask_skip_tags_on_launch: template.ask_skip_tags_on_launch || false,
ask_tags_on_launch: template.ask_tags_on_launch || false,
webhook_url: template?.related?.webhook_receiver
? `${urlOrigin}${template.related.webhook_receiver}`
: '',

View File

@ -189,7 +189,9 @@ describe('<WorkflowJobTemplateForm/>', () => {
'FieldWithPrompt[label="Inventory"]',
'FieldWithPrompt[label="Limit"]',
'FieldWithPrompt[label="Source control branch"]',
'FormGroup[label="Labels"]',
'FieldWithPrompt[label="Labels"]',
'FieldWithPrompt[label="Skip Tags"]',
'FieldWithPrompt[label="Job Tags"]',
'VariablesField',
];

View File

@ -47,6 +47,16 @@ options:
description:
- Variables which will be made available to jobs ran inside the workflow.
type: dict
job_tags:
description:
- Comma separated list of the tags to use for the job template.
type: str
ask_tags_on_launch:
description:
- Prompt user for job tags on launch.
type: bool
aliases:
- ask_tags
organization:
description:
- Organization the workflow job template exists in.
@ -85,6 +95,22 @@ options:
description:
- Prompt user for limit on launch of this workflow job template
type: bool
ask_labels_on_launch:
description:
- Prompt user for labels on launch.
type: bool
aliases:
- ask_labels
ask_skip_tags_on_launch:
description:
- Prompt user for job tags to skip on launch.
type: bool
aliases:
- ask_skip_tags
skip_tags:
description:
- Comma separated list of the tags to skip for the job template.
type: str
webhook_service:
description:
- Service that webhook requests will be accepted from
@ -665,11 +691,15 @@ def main():
copy_from=dict(),
description=dict(),
extra_vars=dict(type='dict'),
job_tags=dict(),
skip_tags=dict(),
organization=dict(),
survey_spec=dict(type='dict', aliases=['survey']),
survey_enabled=dict(type='bool'),
allow_simultaneous=dict(type='bool'),
ask_variables_on_launch=dict(type='bool'),
ask_labels_on_launch=dict(type='bool', aliases=['ask_labels']),
ask_skip_tags_on_launch=dict(type='bool', aliases=['ask_skip_tags']),
inventory=dict(),
limit=dict(),
scm_branch=dict(),
@ -752,7 +782,11 @@ def main():
'ask_scm_branch_on_launch',
'ask_limit_on_launch',
'ask_variables_on_launch',
'ask_labels_on_launch',
'ask_skip_tags_on_launch',
'webhook_service',
'job_tags',
'skip_tags',
):
field_val = module.params.get(field_name)
if field_val is not None:

View File

@ -18,6 +18,8 @@ def test_create_workflow_job_template(run_module, admin_user, organization, surv
'survey_spec': survey_spec,
'survey_enabled': True,
'state': 'present',
'job_tags': '',
'skip_tags': '',
},
admin_user,
)
@ -35,7 +37,16 @@ def test_create_workflow_job_template(run_module, admin_user, organization, surv
@pytest.mark.django_db
def test_create_modify_no_survey(run_module, admin_user, organization, survey_spec):
result = run_module('workflow_job_template', {'name': 'foo-workflow', 'organization': organization.name}, admin_user)
result = run_module(
'workflow_job_template',
{
'name': 'foo-workflow',
'organization': organization.name,
'job_tags': '',
'skip_tags': '',
},
admin_user,
)
assert not result.get('failed', False), result.get('msg', result)
assert result.get('changed', False), result