diff options
author | Sarabraj Singh <sarsingh@redhat.com> | 2022-08-03 20:27:35 +0200 |
---|---|---|
committer | Alan Rominger <arominge@redhat.com> | 2022-09-22 21:18:47 +0200 |
commit | 663ef2cc6413c0cdb26392bb046b37fe564fb546 (patch) | |
tree | f02e61637bbc0e1a1b2f0ae74fca43a7ce710e50 | |
parent | Change ask_job_slicing_on_launch to ask_job_slice_count_on_launch to match api (diff) | |
download | awx-663ef2cc6413c0cdb26392bb046b37fe564fb546.tar.xz awx-663ef2cc6413c0cdb26392bb046b37fe564fb546.zip |
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>
24 files changed, 365 insertions, 78 deletions
diff --git a/awx/api/serializers.py b/awx/api/serializers.py index 01664f03fd..5d7b90ae51 100644 --- a/awx/api/serializers.py +++ b/awx/api/serializers.py @@ -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 diff --git a/awx/api/views/__init__.py b/awx/api/views/__init__.py index 00d59484d8..012e320bc8 100644 --- a/awx/api/views/__init__.py +++ b/awx/api/views/__init__.py @@ -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): diff --git a/awx/main/migrations/0167_jt_prompt_everything_on_launch.py b/awx/main/migrations/0167_jt_prompt_everything_on_launch.py index e0257e7103..b03f42235e 100644 --- a/awx/main/migrations/0167_jt_prompt_everything_on_launch.py +++ b/awx/main/migrations/0167_jt_prompt_everything_on_launch.py @@ -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), + ), ] diff --git a/awx/main/models/jobs.py b/awx/main/models/jobs.py index d71dbc078f..731a3eaf65 100644 --- a/awx/main/models/jobs.py +++ b/awx/main/models/jobs.py @@ -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, diff --git a/awx/main/models/mixins.py b/awx/main/models/mixins.py index 0e38d7288c..df10f0b29f 100644 --- a/awx/main/models/mixins.py +++ b/awx/main/models/mixins.py @@ -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): diff --git a/awx/main/models/unified_jobs.py b/awx/main/models/unified_jobs.py index 21b4f4361b..b3d8bc4bee 100644 --- a/awx/main/models/unified_jobs.py +++ b/awx/main/models/unified_jobs.py @@ -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 diff --git a/awx/main/models/workflow.py b/awx/main/models/workflow.py index 4f52ade6b4..4417807cbd 100644 --- a/awx/main/models/workflow.py +++ b/awx/main/models/workflow.py @@ -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( - blank=True, - default=False, - ) - ask_limit_on_launch = AskForField( + notification_templates_approvals = models.ManyToManyField( + "NotificationTemplate", blank=True, - default=False, + related_name='%(class)s_notification_templates_for_approvals', ) - ask_scm_branch_on_launch = AskForField( - blank=True, - default=False, + admin_role = ImplicitRoleField( + parent_role=['singleton:' + ROLE_SINGLETON_SYSTEM_ADMINISTRATOR, 'organization.workflow_admin_role'], ) - 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', diff --git a/awx/main/tests/factories/fixtures.py b/awx/main/tests/factories/fixtures.py index 200fa0f195..27556d6efe 100644 --- a/awx/main/tests/factories/fixtures.py +++ b/awx/main/tests/factories/fixtures.py @@ -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 diff --git a/awx/main/tests/functional/conftest.py b/awx/main/tests/functional/conftest.py index 2e3563a2b6..4f8b6bc83c 100644 --- a/awx/main/tests/functional/conftest.py +++ b/awx/main/tests/functional/conftest.py @@ -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 diff --git a/awx/main/tests/functional/models/test_workflow.py b/awx/main/tests/functional/models/test_workflow.py index d8fa495c6c..b6df98fe59 100644 --- a/awx/main/tests/functional/models/test_workflow.py +++ b/awx/main/tests/functional/models/test_workflow.py @@ -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): diff --git a/awx/main/tests/unit/api/serializers/test_workflow_serializers.py b/awx/main/tests/unit/api/serializers/test_workflow_serializers.py index 526f06c4c9..0cbf6b7af0 100644 --- a/awx/main/tests/unit/api/serializers/test_workflow_serializers.py +++ b/awx/main/tests/unit/api/serializers/test_workflow_serializers.py @@ -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 diff --git a/awx/main/tests/unit/models/test_survey_models.py b/awx/main/tests/unit/models/test_survey_models.py index 9ec5673cd8..57058930ea 100644 --- a/awx/main/tests/unit/models/test_survey_models.py +++ b/awx/main/tests/unit/models/test_survey_models.py @@ -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) diff --git a/awx/main/tests/unit/models/test_workflow_unit.py b/awx/main/tests/unit/models/test_workflow_unit.py index f8bb1e9c84..65190f92a3 100644 --- a/awx/main/tests/unit/models/test_workflow_unit.py +++ b/awx/main/tests/unit/models/test_workflow_unit.py @@ -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', + ] diff --git a/awx/main/tests/unit/test_access.py b/awx/main/tests/unit/test_access.py index 547af7b42c..0059cb4984 100644 --- a/awx/main/tests/unit/test_access.py +++ b/awx/main/tests/unit/test_access.py @@ -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): diff --git a/awx/ui/src/components/LaunchPrompt/LaunchPrompt.test.js b/awx/ui/src/components/LaunchPrompt/LaunchPrompt.test.js index 27263d479c..d4e3cee1ef 100644 --- a/awx/ui/src/components/LaunchPrompt/LaunchPrompt.test.js +++ b/awx/ui/src/components/LaunchPrompt/LaunchPrompt.test.js @@ -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' diff --git a/awx/ui/src/screens/Template/JobTemplateAdd/JobTemplateAdd.test.js b/awx/ui/src/screens/Template/JobTemplateAdd/JobTemplateAdd.test.js index 7c119a522d..9fffccbb33 100644 --- a/awx/ui/src/screens/Template/JobTemplateAdd/JobTemplateAdd.test.js +++ b/awx/ui/src/screens/Template/JobTemplateAdd/JobTemplateAdd.test.js @@ -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, diff --git a/awx/ui/src/screens/Template/JobTemplateEdit/JobTemplateEdit.test.js b/awx/ui/src/screens/Template/JobTemplateEdit/JobTemplateEdit.test.js index 500143973f..2ada0105d5 100644 --- a/awx/ui/src/screens/Template/JobTemplateEdit/JobTemplateEdit.test.js +++ b/awx/ui/src/screens/Template/JobTemplateEdit/JobTemplateEdit.test.js @@ -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, diff --git a/awx/ui/src/screens/Template/WorkflowJobTemplateAdd/WorkflowJobTemplateAdd.test.js b/awx/ui/src/screens/Template/WorkflowJobTemplateAdd/WorkflowJobTemplateAdd.test.js index a6cb0e1969..8a0c55cd06 100644 --- a/awx/ui/src/screens/Template/WorkflowJobTemplateAdd/WorkflowJobTemplateAdd.test.js +++ b/awx/ui/src/screens/Template/WorkflowJobTemplateAdd/WorkflowJobTemplateAdd.test.js @@ -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: '', diff --git a/awx/ui/src/screens/Template/WorkflowJobTemplateEdit/WorkflowJobTemplateEdit.test.js b/awx/ui/src/screens/Template/WorkflowJobTemplateEdit/WorkflowJobTemplateEdit.test.js index cb56e82ef0..56c99782c1 100644 --- a/awx/ui/src/screens/Template/WorkflowJobTemplateEdit/WorkflowJobTemplateEdit.test.js +++ b/awx/ui/src/screens/Template/WorkflowJobTemplateEdit/WorkflowJobTemplateEdit.test.js @@ -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: '', diff --git a/awx/ui/src/screens/Template/shared/WorkflowJobTemplate.helptext.js b/awx/ui/src/screens/Template/shared/WorkflowJobTemplate.helptext.js index eba476a214..a8f29f7bc6 100644 --- a/awx/ui/src/screens/Template/shared/WorkflowJobTemplate.helptext.js +++ b/awx/ui/src/screens/Template/shared/WorkflowJobTemplate.helptext.js @@ -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> diff --git a/awx/ui/src/screens/Template/shared/WorkflowJobTemplateForm.js b/awx/ui/src/screens/Template/shared/WorkflowJobTemplateForm.js index 1b9f1f9511..30e9ac8668 100644 --- a/awx/ui/src/screens/Template/shared/WorkflowJobTemplateForm.js +++ b/awx/ui/src/screens/Template/shared/WorkflowJobTemplateForm.js @@ -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}` : '', diff --git a/awx/ui/src/screens/Template/shared/WorkflowJobTemplateForm.test.js b/awx/ui/src/screens/Template/shared/WorkflowJobTemplateForm.test.js index 17234df88c..65b5f59522 100644 --- a/awx/ui/src/screens/Template/shared/WorkflowJobTemplateForm.test.js +++ b/awx/ui/src/screens/Template/shared/WorkflowJobTemplateForm.test.js @@ -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', ]; diff --git a/awx_collection/plugins/modules/workflow_job_template.py b/awx_collection/plugins/modules/workflow_job_template.py index afc792e1f1..93eb451503 100644 --- a/awx_collection/plugins/modules/workflow_job_template.py +++ b/awx_collection/plugins/modules/workflow_job_template.py @@ -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: diff --git a/awx_collection/test/awx/test_workflow_job_template.py b/awx_collection/test/awx/test_workflow_job_template.py index c5448b23aa..60a4fff7cf 100644 --- a/awx_collection/test/awx/test_workflow_job_template.py +++ b/awx_collection/test/awx/test_workflow_job_template.py @@ -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 |