From a5043029c16e22ccf1f711125fefacc11cdb9be3 Mon Sep 17 00:00:00 2001 From: mabashian Date: Thu, 1 Mar 2018 15:24:50 -0500 Subject: [PATCH] Implemented the ability to specify credentials when creating a scheduled job run. Added validation for removing but not replacing default credentials. --- .../client/features/templates/list.view.html | 2 +- .../features/templates/templates.strings.js | 5 +- awx/ui/client/lib/models/Schedule.js | 38 +++++ awx/ui/client/lib/models/index.js | 2 + .../factories/schedule-post.factory.js | 148 +++++++++++++----- .../src/scheduler/schedulerAdd.controller.js | 1 + .../src/scheduler/schedulerEdit.controller.js | 29 +++- awx/ui/client/src/shared/generator-helpers.js | 8 +- .../src/templates/prompt/prompt.block.less | 4 + .../src/templates/prompt/prompt.controller.js | 11 +- .../src/templates/prompt/prompt.partial.html | 2 +- .../src/templates/prompt/prompt.service.js | 2 +- .../prompt-credential.controller.js | 62 +++++++- .../credential/prompt-credential.partial.html | 9 +- .../inventory/prompt-inventory.controller.js | 2 +- .../inventory/prompt-inventory.partial.html | 6 +- 16 files changed, 264 insertions(+), 67 deletions(-) create mode 100644 awx/ui/client/lib/models/Schedule.js diff --git a/awx/ui/client/features/templates/list.view.html b/awx/ui/client/features/templates/list.view.html index 7759287085..bd0511eddf 100644 --- a/awx/ui/client/features/templates/list.view.html +++ b/awx/ui/client/features/templates/list.view.html @@ -118,5 +118,5 @@ query-set="querySet"> - + diff --git a/awx/ui/client/features/templates/templates.strings.js b/awx/ui/client/features/templates/templates.strings.js index 015a007569..1b949fac9c 100644 --- a/awx/ui/client/features/templates/templates.strings.js +++ b/awx/ui/client/features/templates/templates.strings.js @@ -55,7 +55,8 @@ function TemplatesStrings (BaseString) { VALID_DECIMAL: t.s('Please enter an answer that is a decimal number.'), PLAYBOOK_RUN: t.s('Playbook Run'), CHECK: t.s('Check'), - NO_CREDS_MATCHING_TYPE: t.s('No Credentials Matching This Type Have Been Created'), + NO_CREDS_MATCHING_TYPE: t.s('No Credentials Matching This Type Have Been Created'), + CREDENTIAL_TYPE_MISSING: typeLabel => t.s('This job template has a default {{typeLabel}} credential which must be included or replaced before proceeding.', { typeLabel }) }; ns.alert = { @@ -85,7 +86,7 @@ function TemplatesStrings (BaseString) { ns.warnings = { WORKFLOW_RESTRICTED_COPY: t.s('You do not have access to all resources used by this workflow. Resources that you don\'t have access to will not be copied and will result in an incomplete workflow.') - } + }; } TemplatesStrings.$inject = ['BaseStringService']; diff --git a/awx/ui/client/lib/models/Schedule.js b/awx/ui/client/lib/models/Schedule.js new file mode 100644 index 0000000000..b6bd3cf60f --- /dev/null +++ b/awx/ui/client/lib/models/Schedule.js @@ -0,0 +1,38 @@ +let Base; +let $http; + +function postCredential (params) { + const req = { + method: 'POST', + url: `${this.path}${params.id}/credentials/` + }; + + if (params.data) { + req.data = params.data; + } + + return $http(req); +} + +function ScheduleModel (method, resource, config) { + Base.call(this, 'schedules'); + + this.Constructor = ScheduleModel; + this.postCredential = postCredential.bind(this); + + return this.create(method, resource, config); +} + +function ScheduleModelLoader (BaseModel, _$http_) { + Base = BaseModel; + $http = _$http_; + + return ScheduleModel; +} + +ScheduleModelLoader.$inject = [ + 'BaseModel', + '$http' +]; + +export default ScheduleModelLoader; diff --git a/awx/ui/client/lib/models/index.js b/awx/ui/client/lib/models/index.js index 950d0d2c73..fdecf596fd 100644 --- a/awx/ui/client/lib/models/index.js +++ b/awx/ui/client/lib/models/index.js @@ -16,6 +16,7 @@ import ModelsStrings from '~models/models.strings'; import NotificationTemplate from '~models/NotificationTemplate'; import Organization from '~models/Organization'; import Project from '~models/Project'; +import Schedule from '~models/Schedule'; import UnifiedJobTemplate from '~models/UnifiedJobTemplate'; import WorkflowJob from '~models/WorkflowJob'; import WorkflowJobTemplate from '~models/WorkflowJobTemplate'; @@ -43,6 +44,7 @@ angular .service('NotificationTemplate', NotificationTemplate) .service('OrganizationModel', Organization) .service('ProjectModel', Project) + .service('ScheduleModel', Schedule) .service('UnifiedJobTemplateModel', UnifiedJobTemplate) .service('WorkflowJobModel', WorkflowJob) .service('WorkflowJobTemplateModel', WorkflowJobTemplate) diff --git a/awx/ui/client/src/scheduler/factories/schedule-post.factory.js b/awx/ui/client/src/scheduler/factories/schedule-post.factory.js index 8911b51265..706dccf5b8 100644 --- a/awx/ui/client/src/scheduler/factories/schedule-post.factory.js +++ b/awx/ui/client/src/scheduler/factories/schedule-post.factory.js @@ -1,36 +1,37 @@ export default - function SchedulePost(Rest, ProcessErrors, RRuleToAPI, Wait, $q) { + function SchedulePost(Rest, ProcessErrors, RRuleToAPI, Wait, $q, Schedule) { return function(params) { var scope = params.scope, url = params.url, scheduler = params.scheduler, mode = params.mode, - schedule = (params.schedule) ? params.schedule : {}, + scheduleData = (params.schedule) ? params.schedule : {}, promptData = params.promptData, + priorCredentials = params.priorCredentials ? params.priorCredentials : [], newSchedule, rrule, extra_vars; let deferred = $q.defer(); if (scheduler.isValid()) { Wait('start'); newSchedule = scheduler.getValue(); rrule = scheduler.getRRule(); - schedule.name = newSchedule.name; - schedule.rrule = RRuleToAPI(rrule.toString(), scope); - schedule.description = (/error/.test(rrule.toText())) ? '' : rrule.toText(); + scheduleData.name = newSchedule.name; + scheduleData.rrule = RRuleToAPI(rrule.toString(), scope); + scheduleData.description = (/error/.test(rrule.toText())) ? '' : rrule.toText(); if (scope.isFactCleanup) { extra_vars = { "older_than": scope.scheduler_form.keep_amount.$viewValue + scope.scheduler_form.keep_unit.$viewValue.value, "granularity": scope.scheduler_form.granularity_keep_amount.$viewValue + scope.scheduler_form.granularity_keep_unit.$viewValue.value }; - schedule.extra_data = JSON.stringify(extra_vars); + scheduleData.extra_data = JSON.stringify(extra_vars); } else if (scope.cleanupJob) { extra_vars = { "days" : scope.scheduler_form.schedulerPurgeDays.$viewValue }; - schedule.extra_data = JSON.stringify(extra_vars); + scheduleData.extra_data = JSON.stringify(extra_vars); } else if(scope.extraVars){ - schedule.extra_data = scope.parseType === 'yaml' ? + scheduleData.extra_data = scope.parseType === 'yaml' ? (scope.extraVars === '---' ? "" : jsyaml.safeLoad(scope.extraVars)) : scope.extraVars; } @@ -40,10 +41,10 @@ export default var fld = promptData.surveyQuestions[i].variable; // grab all survey questions that have answers if(promptData.surveyQuestions[i].required || (promptData.surveyQuestions[i].required === false && promptData.surveyQuestions[i].model.toString()!=="")) { - if(!schedule.extra_data) { - schedule.extra_data = {}; + if(!scheduleData.extra_data) { + scheduleData.extra_data = {}; } - schedule.extra_data[fld] = promptData.surveyQuestions[i].model; + scheduleData.extra_data[fld] = promptData.surveyQuestions[i].model; } if(promptData.surveyQuestions[i].required === false && _.isEmpty(promptData.surveyQuestions[i].model)) { @@ -55,7 +56,7 @@ export default case "text": case "textarea": if (promptData.surveyQuestions[i].min === 0) { - schedule.extra_data[fld] = ""; + scheduleData.extra_data[fld] = ""; } break; } @@ -64,43 +65,65 @@ export default } if(_.has(promptData, 'prompts.jobType.value.value') && _.get(promptData, 'launchConf.ask_job_type_on_launch')) { - schedule.job_type = promptData.prompts.jobType.templateDefault === promptData.prompts.jobType.value.value ? null : promptData.prompts.jobType.value.value; + scheduleData.job_type = promptData.launchConf.defaults.job_type && promptData.launchConf.defaults.job_type === promptData.prompts.jobType.value.value ? null : promptData.prompts.jobType.value.value; } if(_.has(promptData, 'prompts.tags.value') && _.get(promptData, 'launchConf.ask_tags_on_launch')){ - let templateDefaultJobTags = promptData.prompts.tags.templateDefault.split(','); - schedule.job_tags = (_.isEqual(templateDefaultJobTags.sort(), promptData.prompts.tags.value.map(a => a.value).sort())) ? null : promptData.prompts.tags.value.map(a => a.value).join(); + let templateDefaultJobTags = promptData.launchConf.defaults.job_tags.split(','); + scheduleData.job_tags = (_.isEqual(templateDefaultJobTags.sort(), promptData.prompts.tags.value.map(a => a.value).sort())) ? null : promptData.prompts.tags.value.map(a => a.value).join(); } if(_.has(promptData, 'prompts.skipTags.value') && _.get(promptData, 'launchConf.ask_skip_tags_on_launch')){ - let templateDefaultSkipTags = promptData.prompts.skipTags.templateDefault.split(','); - schedule.skip_tags = (_.isEqual(templateDefaultSkipTags.sort(), promptData.prompts.skipTags.value.map(a => a.value).sort())) ? null : promptData.prompts.skipTags.value.map(a => a.value).join(); + let templateDefaultSkipTags = promptData.launchConf.defaults.skip_tags.split(','); + scheduleData.skip_tags = (_.isEqual(templateDefaultSkipTags.sort(), promptData.prompts.skipTags.value.map(a => a.value).sort())) ? null : promptData.prompts.skipTags.value.map(a => a.value).join(); } if(_.has(promptData, 'prompts.limit.value') && _.get(promptData, 'launchConf.ask_limit_on_launch')){ - schedule.limit = promptData.prompts.limit.templateDefault === promptData.prompts.limit.value ? null : promptData.prompts.limit.value; + scheduleData.limit = promptData.launchConf.defaults.limit && promptData.launchConf.defaults.limit === promptData.prompts.limit.value ? null : promptData.prompts.limit.value; } if(_.has(promptData, 'prompts.verbosity.value.value') && _.get(promptData, 'launchConf.ask_verbosity_on_launch')){ - schedule.verbosity = promptData.prompts.verbosity.templateDefault === promptData.prompts.verbosity.value.value ? null : promptData.prompts.verbosity.value.value; + scheduleData.verbosity = promptData.launchConf.defaults.verbosity && promptData.launchConf.defaults.verbosity === promptData.prompts.verbosity.value.value ? null : promptData.prompts.verbosity.value.value; } if(_.has(promptData, 'prompts.inventory.value') && _.get(promptData, 'launchConf.ask_inventory_on_launch')){ - schedule.inventory = promptData.prompts.inventory.templateDefault.id === promptData.prompts.inventory.value.id ? null : promptData.prompts.inventory.value.id; + scheduleData.inventory = promptData.launchConf.defaults.inventory && promptData.launchConf.defaults.inventory.id === promptData.prompts.inventory.value.id ? null : promptData.prompts.inventory.value.id; } if(_.has(promptData, 'prompts.diffMode.value') && _.get(promptData, 'launchConf.ask_diff_mode_on_launch')){ - schedule.diff_mode = promptData.prompts.diffMode.templateDefault === promptData.prompts.diffMode.value ? null : promptData.prompts.diffMode.value; + scheduleData.diff_mode = promptData.launchConf.defaults.diff_mode && promptData.launchConf.defaults.diff_mode === promptData.prompts.diffMode.value ? null : promptData.prompts.diffMode.value; } - // Credentials gets POST'd to a separate endpoint - // if($scope.promptData.launchConf.ask_credential_on_launch){ - // jobLaunchData.credentials = []; - // promptData.credentials.value.forEach((credential) => { - // jobLaunchData.credentials.push(credential.id); - // }); - // } } Rest.setUrl(url); if (mode === 'add') { - Rest.post(schedule) - .then(() => { - Wait('stop'); - deferred.resolve(); + Rest.post(scheduleData) + .then(({data}) => { + if(_.get(promptData, 'launchConf.ask_credential_on_launch')){ + // This finds the credentials that were selected in the prompt but don't occur + // in the template defaults + let credentialsToPost = promptData.prompts.credentials.value.filter(function(credFromPrompt) { + let defaultCreds = promptData.launchConf.defaults.credentials ? promptData.launchConf.defaults.credentials : []; + return !defaultCreds.some(function(defaultCred) { + return credFromPrompt.id === defaultCred.id; + }); + }); + + let promises = []; + let schedule = new Schedule(); + + credentialsToPost.forEach((credentialToPost) => { + promises.push(schedule.postCredential({ + id: data.id, + data: { + id: credentialToPost.id + } + })); + }); + + $q.all(promises) + .then(() => { + Wait('stop'); + deferred.resolve(); + }); + } else { + Wait('stop'); + deferred.resolve(); + } }) .catch(({data, status}) => { ProcessErrors(scope, data, status, null, { hdr: 'Error!', @@ -110,10 +133,62 @@ export default }); } else { - Rest.put(schedule) - .then(() => { + Rest.put(scheduleData) + .then(({data}) => { + if(_.get(promptData, 'launchConf.ask_credential_on_launch')){ + let credentialsNotInPriorCredentials = promptData.prompts.credentials.value.filter(function(credFromPrompt) { + let defaultCreds = promptData.launchConf.defaults.credentials ? promptData.launchConf.defaults.credentials : []; + return !defaultCreds.some(function(defaultCred) { + return credFromPrompt.id === defaultCred.id; + }); + }); + + let credentialsToAdd = credentialsNotInPriorCredentials.filter(function(credNotInPrior) { + return !priorCredentials.some(function(priorCred) { + return credNotInPrior.id === priorCred.id; + }); + }); + + let credentialsToRemove = priorCredentials.filter(function(priorCred) { + return !credentialsNotInPriorCredentials.some(function(credNotInPrior) { + return priorCred.id === credNotInPrior.id; + }); + }); + + let promises = []; + let schedule = new Schedule(); + + credentialsToAdd.forEach((credentialToAdd) => { + promises.push(schedule.postCredential({ + id: data.id, + data: { + id: credentialToAdd.id + } + })); + }); + + credentialsToRemove.forEach((credentialToRemove) => { + promises.push(schedule.postCredential({ + id: data.id, + data: { + id: credentialToRemove.id, + disassociate: true + } + })); + }); + + $q.all(promises) + .then(() => { + Wait('stop'); + deferred.resolve(); + }); + } else { + Wait('stop'); + deferred.resolve(); + } + Wait('stop'); - deferred.resolve(schedule); + deferred.resolve(scheduleData); }) .catch(({data, status}) => { ProcessErrors(scope, data, status, null, { hdr: 'Error!', @@ -136,5 +211,6 @@ SchedulePost.$inject = 'ProcessErrors', 'RRuleToAPI', 'Wait', - '$q' + '$q', + 'ScheduleModel' ]; diff --git a/awx/ui/client/src/scheduler/schedulerAdd.controller.js b/awx/ui/client/src/scheduler/schedulerAdd.controller.js index 661a708dd6..f3096a1ce1 100644 --- a/awx/ui/client/src/scheduler/schedulerAdd.controller.js +++ b/awx/ui/client/src/scheduler/schedulerAdd.controller.js @@ -104,6 +104,7 @@ export default ['$filter', '$state', '$stateParams', '$http', 'Wait', let watchForPromptChanges = () => { let promptValuesToWatch = [ 'promptData.prompts.inventory.value', + 'promptData.prompts.jobType.value', 'promptData.prompts.verbosity.value', 'missingSurveyValue' ]; diff --git a/awx/ui/client/src/scheduler/schedulerEdit.controller.js b/awx/ui/client/src/scheduler/schedulerEdit.controller.js index b0d8adcb83..a91d3fef7f 100644 --- a/awx/ui/client/src/scheduler/schedulerEdit.controller.js +++ b/awx/ui/client/src/scheduler/schedulerEdit.controller.js @@ -5,7 +5,7 @@ function($filter, $state, $stateParams, Wait, $scope, moment, $rootScope, $http, CreateSelect2, ParseTypeChange, ParentObject, ProcessErrors, Rest, GetBasePath, SchedulerInit, SchedulePost, JobTemplate, $q, Empty, PromptService, RRuleToAPI) { - let schedule, scheduler; + let schedule, scheduler, scheduleCredentials = []; // initial end @ midnight values $scope.schedulerEndHour = "00"; @@ -63,7 +63,8 @@ function($filter, $state, $stateParams, Wait, $scope, moment, scheduler: scheduler, mode: 'edit', schedule: schedule, - promptData: $scope.promptData + promptData: $scope.promptData, + priorCredentials: scheduleCredentials }).then(() => { Wait('stop'); $state.go("^", null, {reload: true}); @@ -254,14 +255,14 @@ function($filter, $state, $stateParams, Wait, $scope, moment, $q.all([jobTemplate.optionsLaunch(ParentObject.id), jobTemplate.getLaunch(ParentObject.id), Rest.get()]) .then((responses) => { let launchOptions = responses[0].data, - launchConf = responses[1].data, - scheduleCredentials = responses[2].data; + launchConf = responses[1].data; + scheduleCredentials = responses[2].data.results; let watchForPromptChanges = () => { let promptValuesToWatch = [ - // credential passwords...? 'promptData.prompts.inventory.value', + 'promptData.prompts.jobType.value', 'promptData.prompts.verbosity.value', 'missingSurveyValue' ]; @@ -283,7 +284,23 @@ function($filter, $state, $stateParams, Wait, $scope, moment, currentValues: data }); - prompts.credentials.value = scheduleCredentials.results.length > 0 ? scheduleCredentials.results : prompts.credentials.value; + let defaultCredsWithoutOverrides = []; + + prompts.credentials.value.forEach((defaultCred) => { + let typeMatches = false; + scheduleCredentials.forEach((scheduleCred) => { + if(defaultCred.credential_type === scheduleCred.credential_type) { + if((!defaultCred.vault_id && !scheduleCred.inputs.vault_id) || (defaultCred.vault_id && scheduleCred.inputs.vault_id && defaultCred.vault_id === scheduleCred.inputs.vault_id)) { + typeMatches = true; + } + } + }); + if(!typeMatches) { + defaultCredsWithoutOverrides.push(defaultCred); + } + }); + + prompts.credentials.value = scheduleCredentials.concat(defaultCredsWithoutOverrides); if(!launchConf.ask_variables_on_launch) { $scope.noVars = true; diff --git a/awx/ui/client/src/shared/generator-helpers.js b/awx/ui/client/src/shared/generator-helpers.js index f2fd9f2f1f..f848be05cd 100644 --- a/awx/ui/client/src/shared/generator-helpers.js +++ b/awx/ui/client/src/shared/generator-helpers.js @@ -560,7 +560,13 @@ angular.module('GeneratorHelpers', [systemStatus.name]) } html += "\" "; html += field.columnNgClass ? " ng-class=\"" + field.columnNgClass + "\"": ""; - html += (options.mode === 'lookup' || options.mode === 'select') ? " ng-click=\"toggle_row(" + list.iterator + ")\"" : ""; + if(options.mode === 'lookup' || options.mode === 'select') { + if (options.input_type === "radio") { + html += " ng-click=\"toggle_row(" + list.iterator + ")\""; + } else { + html += " ng-click=\"toggle_" + list.iterator + "(" + list.iterator + ", true)\""; + } + } html += (field.columnShow) ? Attr(field, 'columnShow') : ""; html += (field.ngBindHtml) ? "ng-bind-html=\"" + field.ngBindHtml + "\" " : ""; html += (field.columnClick) ? "ng-click=\"" + field.columnClick + "\" " : ""; diff --git a/awx/ui/client/src/templates/prompt/prompt.block.less b/awx/ui/client/src/templates/prompt/prompt.block.less index e6cc8601a1..62275ecc0b 100644 --- a/awx/ui/client/src/templates/prompt/prompt.block.less +++ b/awx/ui/client/src/templates/prompt/prompt.block.less @@ -165,3 +165,7 @@ .Prompt-credentialSubSection .select2 { width: 50% !important; } +.Prompt-credentialTypeMissing { + margin-bottom: 20px; + color: @default-err; +} diff --git a/awx/ui/client/src/templates/prompt/prompt.controller.js b/awx/ui/client/src/templates/prompt/prompt.controller.js index c2a4563390..f2f08f9207 100644 --- a/awx/ui/client/src/templates/prompt/prompt.controller.js +++ b/awx/ui/client/src/templates/prompt/prompt.controller.js @@ -65,8 +65,6 @@ export default [ 'Rest', 'GetBasePath', 'ProcessErrors', 'CredentialTypeModel', } })); - vm.promptData.prompts.inventory.templateDefault = _.has(vm, 'promptData.launchConf.defaults.inventory') ? vm.promptData.launchConf.defaults.inventory : null; - vm.promptData.prompts.credentials.templateDefault = _.has(vm, 'promptData.launchConf.defaults.credentials') ? angular.copy(vm.promptData.launchConf.defaults.credentials) : []; vm.promptData.prompts.credentials.passwordsNeededToStart = vm.promptData.launchConf.passwords_needed_to_start; vm.promptData.prompts.credentials.passwords = {}; @@ -99,17 +97,12 @@ export default [ 'Rest', 'GetBasePath', 'ProcessErrors', 'CredentialTypeModel', vm.promptData.prompts.credentials.passwords.vault.push(credPassObj); } }); - } }); + vm.promptData.credentialTypeMissing = []; + vm.promptData.prompts.variables.ignore = vm.promptData.launchConf.ignore_ask_variables; - vm.promptData.prompts.verbosity.templateDefault = vm.promptData.launchConf.defaults.verbosity; - vm.promptData.prompts.jobType.templateDefault = vm.promptData.launchConf.defaults.job_type; - vm.promptData.prompts.limit.templateDefault = vm.promptData.launchConf.defaults.limit; - vm.promptData.prompts.tags.templateDefault = vm.promptData.launchConf.defaults.job_tags; - vm.promptData.prompts.skipTags.templateDefault = vm.promptData.launchConf.defaults.skip_tags; - vm.promptData.prompts.diffMode.templateDefault = vm.promptData.launchConf.defaults.diff_mode; if(vm.promptData.launchConf.ask_inventory_on_launch) { vm.steps.inventory.includeStep = true; diff --git a/awx/ui/client/src/templates/prompt/prompt.partial.html b/awx/ui/client/src/templates/prompt/prompt.partial.html index e2c5c73090..8a99a5f7b0 100644 --- a/awx/ui/client/src/templates/prompt/prompt.partial.html +++ b/awx/ui/client/src/templates/prompt/prompt.partial.html @@ -27,7 +27,7 @@