diff --git a/rundeckapp/grails-app/controllers/rundeck/controllers/MenuController.groovy b/rundeckapp/grails-app/controllers/rundeck/controllers/MenuController.groovy index 87f34b42344..63ac2eba51c 100644 --- a/rundeckapp/grails-app/controllers/rundeck/controllers/MenuController.groovy +++ b/rundeckapp/grails-app/controllers/rundeck/controllers/MenuController.groovy @@ -3124,7 +3124,7 @@ Since: v17''', } - def list = jobSchedulesService.getAllScheduled(uuid) + def list = jobSchedulesService.getAllScheduled(uuid, null) //filter authorized jobs Map projectAuths = [:] def authForProject = { String project -> diff --git a/rundeckapp/grails-app/services/rundeck/services/JobSchedulesService.groovy b/rundeckapp/grails-app/services/rundeck/services/JobSchedulesService.groovy index e3422d5e7e5..cc7e32b8771 100644 --- a/rundeckapp/grails-app/services/rundeck/services/JobSchedulesService.groovy +++ b/rundeckapp/grails-app/services/rundeck/services/JobSchedulesService.groovy @@ -40,7 +40,7 @@ class JobSchedulesService implements SchedulesManager { } @Override - List getAllScheduled(String serverUUID = null, String project = null) { + List getAllScheduled(String serverUUID, String project) { return rundeckJobSchedulesManager.getAllScheduled(serverUUID, project) } diff --git a/rundeckapp/grails-app/services/rundeck/services/RdJobService.groovy b/rundeckapp/grails-app/services/rundeck/services/RdJobService.groovy index c25ab613a49..d62a983c2e6 100644 --- a/rundeckapp/grails-app/services/rundeck/services/RdJobService.groovy +++ b/rundeckapp/grails-app/services/rundeck/services/RdJobService.groovy @@ -1,14 +1,65 @@ package rundeck.services +import com.dtolabs.rundeck.core.authorization.UserAndRolesAuthContext +import com.dtolabs.rundeck.core.common.INodeSet +import com.dtolabs.rundeck.core.jobs.JobLifecycleComponentException +import com.dtolabs.rundeck.core.jobs.JobOption +import com.dtolabs.rundeck.core.jobs.options.JobOptionConfigData +import com.dtolabs.rundeck.plugins.jobs.JobOptionImpl +import com.dtolabs.rundeck.plugins.jobs.JobPersistEventImpl +import grails.compiler.GrailsCompileStatic +import grails.events.annotation.Publisher import groovy.transform.CompileStatic +import groovy.transform.TypeCheckingMode +import groovy.util.logging.Log4j2 +import org.rundeck.app.authorization.AppAuthContextProcessor +import org.rundeck.app.components.RundeckJobDefinitionManager +import org.rundeck.app.components.jobs.ImportedJob +import org.rundeck.app.data.job.converters.ScheduledExecutionToJobConverter +import org.rundeck.app.data.job.schedule.DefaultJobDataChangeDetector import org.rundeck.app.data.model.v1.DeletionResult import org.rundeck.app.data.model.v1.job.JobData +import org.rundeck.app.data.model.v1.job.component.JobComponentData import org.rundeck.app.data.model.v1.query.JobQueryInputData import org.rundeck.app.data.providers.v1.job.JobDataProvider +import org.rundeck.app.events.LogJobChangeEvent +import org.rundeck.app.job.component.JobComponentDataImportExport +import org.rundeck.app.jobs.options.JobOptionConfigPluginAttributes +import org.rundeck.core.auth.AuthConstants +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.web.context.request.RequestContextHolder +import rundeck.ScheduledExecution +import rundeck.data.job.RdJob +import rundeck.data.job.RdOption +import rundeck.data.job.reference.JobReferenceImpl +import rundeck.data.job.reference.JobRevReferenceImpl +import rundeck.data.validation.exception.DataValidationException +import rundeck.services.audit.JobUpdateAuditEvent +import rundeck.services.data.ScheduledExecutionDataService -@CompileStatic +import javax.servlet.http.HttpSession + +@GrailsCompileStatic +@Log4j2 class RdJobService { + @Autowired + ScheduledExecutionDataService scheduledExecutionDataService + @Autowired + RundeckJobDefinitionManager rundeckJobDefinitionManager + @Autowired + FrameworkService frameworkService + @Autowired + JobSchedulesService jobSchedulesService + @Autowired + JobSchedulerService jobSchedulerService + @Autowired + AppAuthContextProcessor rundeckAuthContextProcessor + @Autowired + ScheduledExecutionService scheduledExecutionService + @Autowired + JobLifecycleComponentService jobLifecycleComponentService + JobDataProvider jobDataProvider JobData getJobByIdOrUuid(Serializable id) { @@ -37,8 +88,36 @@ class RdJobService { return jobDataProvider.findByUuid(uuid) } + @CompileStatic(TypeCheckingMode.SKIP) JobData saveJob(JobData job) { - return jobDataProvider.save(job) + def session = getSession() + def authCtx = frameworkService.userAuthContext(session) as UserAndRolesAuthContext + ScheduledExecution se = job.uuid ? scheduledExecutionDataService.findByUuid(job.uuid) : null + boolean isnew = false + if(!se) { + isnew = true + se = new ScheduledExecution() + } + RdJob rdJob = (RdJob)job + if(!rdJob.validate()) { + throw new DataValidationException(rdJob) + } + LogJobChangeEvent logEvent = new LogJobChangeEvent(isnew ? 'create' : 'update','GormJobDataProvider.save', session.user) + JobChangeData jobChangeData = detectJobChanges(se, rdJob, logEvent) + runComponentBeforeSave(authCtx.username, rdJob) + se.project = rdJob.project + authorizeEditAndUpdateJobUserAndRoles(authCtx, se, rdJob) + def importedJob = validateComponents(se, rdJob) + if(rdJob.errors.hasErrors()) throw new DataValidationException(rdJob) + + rundeckJobDefinitionManager.persistComponents(importedJob,authCtx) + def saved = jobDataProvider.save(rdJob) + def savedSe = scheduledExecutionDataService.findByUuid(saved.uuid) + rundeckJobDefinitionManager.waspersisted(importedJob, authCtx) + rescheduleJob(savedSe, jobChangeData) + publishJobUpdateAuditEvent(savedSe, isnew) + publishLogJobChangeEvent(savedSe, logEvent) + return ScheduledExecutionToJobConverter.convert(savedSe) } DeletionResult delete(String id) { @@ -51,4 +130,204 @@ class RdJobService { def listJobs(JobQueryInputData jobQueryInputData) { jobDataProvider.queryJobs(jobQueryInputData) } + + @CompileStatic(TypeCheckingMode.SKIP) + def getSession() { + RequestContextHolder.currentRequestAttributes().getSession() + } + + @Publisher('log.job.change.event') + LogJobChangeEvent publishLogJobChangeEvent(ScheduledExecution se, LogJobChangeEvent event) { + event.jobData = se + return event + } + + @Publisher('audit.job.update') + JobUpdateAuditEvent publishJobUpdateAuditEvent(ScheduledExecution se, boolean isnew) { + return new JobUpdateAuditEvent(jobUuid: se.uuid, project: se.project, fullName: se.generateFullName(), isnew: isnew) + } + + void runComponentBeforeSave(String username, RdJob rdJob) { + INodeSet nodeSet = scheduledExecutionService.getNodes(rdJob,null) + JobPersistEventImpl jobPersistEvent = new JobPersistEventImpl( + rdJob.jobName, + rdJob.project, + convertToJobOptions(rdJob.optionSet), + nodeSet, + username, + rdJob.nodeConfig.filter + ) + def jobEventStatus + try { + jobEventStatus = jobLifecycleComponentService?.beforeJobSave(rdJob.project, jobPersistEvent) + } catch (JobLifecycleComponentException exception) { + exception.printStackTrace() + log.debug("JobLifecycle error: " + exception.message, exception) + rdJob.errors.reject( + 'scheduledExecution.plugin.error.message', + ['Job Lifecycle: ' + exception.message].toArray(), + "A Plugin returned an error: " + exception.message + ) + return + } + if(jobEventStatus?.isUseNewValues()) { + rdJob.optionSet = jobEventStatus.getOptions() + .collect {opt -> RdOption.convertFromJobOption(opt, rdJob.optionSet?.find { it.name == opt.name })} as SortedSet + rdJob.validate() + } + } + + ImportedJob validateComponents(ScheduledExecution se, RdJob rdJob) { + def associations = [:] as Map + rundeckJobDefinitionManager.jobDefinitionComponents.each{ k, val -> + if(!(val instanceof JobComponentDataImportExport)) { + log.warn("Job component {} cannot be imported to the job data because no importer is defined", k) + return + } + JobComponentDataImportExport importer = (JobComponentDataImportExport)val + if(!rdJob.components.containsKey(importer.componentKey)) return + def data = importer.importFromJobData(rdJob) + if(data) associations[k] = data + } + def importedJob = RundeckJobDefinitionManager.importedJob(se,associations) + def updatedJob = rundeckJobDefinitionManager.updateJob(se, importedJob, [:]) + validateComponentsExist(rdJob) + def rptSet = rundeckJobDefinitionManager.validateImportedJob(updatedJob) + rptSet.validations.each { componentName, rpt -> + rpt.errors.each { k, v -> + rdJob.errors.rejectValue("components", + "jobData.components.invalidconfiguration", + [componentName, k, v] as Object[], + 'Job Component: {0} invalid config: {1} : {2}') + } + } + return updatedJob + } + + void validateComponentsExist(RdJob rdJob) { + def jobDefinitionComponentKeys = rundeckJobDefinitionManager.jobDefinitionComponents.values().findAll{it instanceof JobComponentDataImportExport }.collect { ((JobComponentDataImportExport)it).componentKey } + rdJob.components.each { String componentName, JobComponentData value -> + if (!jobDefinitionComponentKeys.contains(componentName)) { + rdJob.errors.rejectValue("components", + "jobData.components.notfound", + [componentName] as Object[], + 'Job Component of type: {0} could not be found') + } + } + } + + JobChangeData detectJobChanges(ScheduledExecution se, RdJob rdJob, LogJobChangeEvent logEvent) { + JobChangeData jobChangeData = new JobChangeData() + if(!se.id) return jobChangeData + String oldjobname = se.generateJobScheduledName() + String oldjobgroup = se.generateJobGroupName() + jobChangeData.isScheduled = jobSchedulesService.isScheduled(se.uuid) + DefaultJobDataChangeDetector detector = new DefaultJobDataChangeDetector( + localScheduled: se.scheduled, + originalCron: se.generateCrontabExression(), + originalSchedule: se.scheduleEnabled, + originalExecution: se.executionEnabled, + originalTz: se.timeZone, + originalRef: new JobRevReferenceImpl( + id: se.extid, + jobName: se.jobName, + groupPath: se.groupPath, + project: se.project, + version: se.version + ) + ) + jobChangeData.renamed = detector.wasRenamed(rdJob) + + if(jobChangeData.renamed){ + logEvent.changeinfo.rename = true + logEvent.changeinfo.origName = oldjobname + logEvent.changeinfo.origGroup = oldjobgroup + jobChangeData.scheduledJobName = oldjobname + jobChangeData.scheduledGroupPath = oldjobgroup + } + + boolean schedulingWasChanged = detector.schedulingWasChanged(se) + if(frameworkService.isClusterModeEnabled()){ + if (schedulingWasChanged) { + JobReferenceImpl jobReference = se.asReference() as JobReferenceImpl + jobReference.setOriginalQuartzJobName(oldjobname) + jobReference.setOriginalQuartzGroupName(oldjobgroup) + jobChangeData.scheduleOwnerModified = jobSchedulerService.updateScheduleOwner(jobReference) + if (jobChangeData.scheduleOwnerModified) { + rdJob.serverNodeUUID = frameworkService.serverUUID + } + } + if (!rdJob.serverNodeUUID) { + rdJob.serverNodeUUID = frameworkService.serverUUID + } + } + + jobChangeData + } + + @CompileStatic(TypeCheckingMode.SKIP) + void authorizeEditAndUpdateJobUserAndRoles(UserAndRolesAuthContext authContext, ScheduledExecution se, RdJob rdJob) { + def authAction = se.id ? AuthConstants.ACTION_UPDATE : AuthConstants.ACTION_CREATE + se.user = authContext.username + se.userRoles = authContext.roles as List + if (!rundeckAuthContextProcessor.authorizeProjectJobAll(authContext, se, [authAction], se.project)) { + rdJob.errors.rejectValue('jobName', 'ScheduledExecution.jobName.unauthorized', [authAction, rdJob.jobName].toArray(), 'Unauthorized action: {0} for value: {1}') + rdJob.errors.rejectValue('groupPath', 'ScheduledExecution.groupPath.unauthorized', [ authAction, rdJob.groupPath].toArray(), 'Unauthorized action: {0} for value: {1}') + return + } + } + + void rescheduleJob(ScheduledExecution se, JobChangeData jobChangeData) { + scheduledExecutionService.rescheduleJob(se, jobChangeData.isScheduled, + jobChangeData.scheduledJobName, jobChangeData.scheduledGroupPath, false, + !jobChangeData.schedulingWasChanged || !jobChangeData.scheduleOwnerModified) + } + + SortedSet convertToJobOptions(SortedSet rdOptions) { + def opts = new TreeSet() + if(!rdOptions) return opts + + opts.addAll(rdOptions.collect {opt -> + JobOptionConfigData jobOptionConfigData= new JobOptionConfigData() + jobOptionConfigData.addConfig(new JobOptionConfigPluginAttributes(opt.configMap)) + + JobOptionImpl.builder() + .name(opt.name) + .description(opt.description) + .defaultValue(opt.defaultValue) + .delimiter(opt.delimiter) + .defaultStoragePath(opt.defaultStoragePath) + .isDate(opt.isDate) + .dateFormat(opt.dateFormat) + .regex(opt.regex) + .enforced(opt.enforced) + .hidden(opt.hidden) + .optionType(opt.optionType) + .label(opt.label) + .required(opt.required) + .realValuesUrl(opt.realValuesUrl) + .sortIndex(opt.sortIndex) + .optionValues(opt.optionValues) + .optionValuesPluginType(opt.optionValuesPluginType) + .secureExposed(opt.secureExposed) + .secureInput(opt.secureInput) + .configData(jobOptionConfigData) + .multivalueAllSelected(opt.multivalueAllSelected) + .multivalued(opt.multivalued) + .sortValues(opt.sortValues) + .valuesListDelimiter(opt.valuesListDelimiter) + .valuesList(opt.valuesList) + .build() + }) + opts + } + + static class JobChangeData { + String scheduledJobName + String scheduledGroupPath + boolean isScheduled + boolean renamed + boolean scheduleOwnerModified + boolean schedulingWasChanged + } } diff --git a/rundeckapp/grails-app/services/rundeck/services/ScheduledExecutionService.groovy b/rundeckapp/grails-app/services/rundeck/services/ScheduledExecutionService.groovy index ad6444fe911..9c562bb9197 100644 --- a/rundeckapp/grails-app/services/rundeck/services/ScheduledExecutionService.groovy +++ b/rundeckapp/grails-app/services/rundeck/services/ScheduledExecutionService.groovy @@ -217,6 +217,7 @@ class ScheduledExecutionService implements ApplicationContextAware, Initializing UserDataProvider userDataProvider JobDataProvider jobDataProvider UserService userService + RdJobService rdJobService @Override void afterPropertiesSet() throws Exception { @@ -237,7 +238,7 @@ class ScheduledExecutionService implements ApplicationContextAware, Initializing Map getPropertiesMapping() { ConfigPropertiesMapping } JobData saveJob(JobData job) { - jobDataProvider.save(job) + rdJobService.saveJob(job) } /** * Return project config for node cache delay @@ -810,7 +811,7 @@ class ScheduledExecutionService implements ApplicationContextAware, Initializing * @param serverUUID */ def unscheduleJobs(String serverUUID=null){ - def schedJobs = serverUUID ? jobSchedulesService.getAllScheduled(serverUUID) : jobSchedulesService.getAllScheduled() + def schedJobs = jobSchedulesService.getAllScheduled(serverUUID, null) schedJobs.each { ScheduledExecution se -> def jobname = se.generateJobScheduledName() def groupname = se.generateJobGroupName() diff --git a/rundeckapp/src/main/groovy/org/rundeck/app/data/job/converters/OrchestratorUpdater.groovy b/rundeckapp/src/main/groovy/org/rundeck/app/data/job/converters/OrchestratorUpdater.groovy index f9ca5431ba4..74e95c465f6 100644 --- a/rundeckapp/src/main/groovy/org/rundeck/app/data/job/converters/OrchestratorUpdater.groovy +++ b/rundeckapp/src/main/groovy/org/rundeck/app/data/job/converters/OrchestratorUpdater.groovy @@ -1,13 +1,13 @@ package org.rundeck.app.data.job.converters import com.fasterxml.jackson.databind.ObjectMapper +import org.rundeck.app.data.model.v1.job.orchestrator.OrchestratorData import rundeck.Orchestrator -import rundeck.data.job.RdOrchestrator class OrchestratorUpdater { static ObjectMapper mapper = new ObjectMapper() - static void updateOrchestrator(Orchestrator orchestrator, RdOrchestrator rdo) { + static void updateOrchestrator(Orchestrator orchestrator, OrchestratorData rdo) { orchestrator.type = rdo.type orchestrator.content = mapper.writeValueAsString(rdo.configuration) } diff --git a/rundeckapp/src/main/groovy/org/rundeck/app/data/job/converters/ScheduledExecutionFromRdJobUpdater.groovy b/rundeckapp/src/main/groovy/org/rundeck/app/data/job/converters/ScheduledExecutionFromRdJobUpdater.groovy index 637b095e4ed..de0be96133a 100644 --- a/rundeckapp/src/main/groovy/org/rundeck/app/data/job/converters/ScheduledExecutionFromRdJobUpdater.groovy +++ b/rundeckapp/src/main/groovy/org/rundeck/app/data/job/converters/ScheduledExecutionFromRdJobUpdater.groovy @@ -3,34 +3,25 @@ package org.rundeck.app.data.job.converters import com.dtolabs.rundeck.plugins.option.OptionValue import com.fasterxml.jackson.databind.ObjectMapper import grails.compiler.GrailsCompileStatic -import grails.util.Holders -import groovy.transform.CompileStatic -import org.rundeck.app.components.RundeckJobDefinitionManager -import rundeck.data.job.RdJob -import rundeck.data.job.RdLogConfig -import rundeck.data.job.RdNodeConfig -import rundeck.data.job.RdNotification -import rundeck.data.job.RdOption -import rundeck.data.job.RdOptionValue -import rundeck.data.job.RdOrchestrator -import rundeck.data.job.RdSchedule -import rundeck.data.job.RdWorkflow -import rundeck.data.job.RdWorkflowStep -import rundeck.CommandExec -import rundeck.JobExec +import org.rundeck.app.data.model.v1.job.JobData +import org.rundeck.app.data.model.v1.job.config.LogConfig +import org.rundeck.app.data.model.v1.job.config.NodeConfig +import org.rundeck.app.data.model.v1.job.notification.NotificationData +import org.rundeck.app.data.model.v1.job.option.OptionData +import org.rundeck.app.data.model.v1.job.orchestrator.OrchestratorData +import org.rundeck.app.data.model.v1.job.schedule.ScheduleData import rundeck.Notification import rundeck.Option import rundeck.Orchestrator -import rundeck.PluginStep import rundeck.ScheduledExecution import rundeck.Workflow -import rundeck.WorkflowStep +import rundeck.data.job.RdOptionValue @GrailsCompileStatic class ScheduledExecutionFromRdJobUpdater { static ObjectMapper mapper = new ObjectMapper() - static void update(ScheduledExecution se, RdJob job) { + static void update(ScheduledExecution se, JobData job) { se.uuid = job.uuid if(!se.id && !se.uuid) se.uuid = UUID.randomUUID().toString() se.jobName = job.jobName @@ -63,7 +54,7 @@ class ScheduledExecutionFromRdJobUpdater { updatePluginConfig(se, job) } - static updateOrchestrator(ScheduledExecution se, RdOrchestrator rdo) { + static updateOrchestrator(ScheduledExecution se, OrchestratorData rdo) { if(!se.orchestrator && !rdo) return if(se.orchestrator && !rdo) { se.orchestrator = null @@ -73,11 +64,11 @@ class ScheduledExecutionFromRdJobUpdater { OrchestratorUpdater.updateOrchestrator(se.orchestrator, rdo) } - static updatePluginConfig(ScheduledExecution se, RdJob job) { + static updatePluginConfig(ScheduledExecution se, JobData job) { se.pluginConfig = mapper.writeValueAsString(job.pluginConfigMap) } - static updateNotifications(ScheduledExecution se, RdJob job) { + static updateNotifications(ScheduledExecution se, JobData job) { //remove all notifications for(int i = 0; i < se.notifications?.size(); i++) { def notif = se.notifications[i] @@ -93,14 +84,14 @@ class ScheduledExecutionFromRdJobUpdater { } } - static void updateNotification(Notification n, RdNotification rdn) { + static void updateNotification(Notification n, NotificationData rdn) { n.type = rdn.type n.configuration = rdn.configuration n.eventTrigger = rdn.eventTrigger n.format = rdn.format } - static void updateJobOptions(ScheduledExecution se, SortedSet rdopts) { + static void updateJobOptions(ScheduledExecution se, SortedSet rdopts) { //remove old options for(int i = 0; i < se.options?.size(); i++) { def opt = se.options[i] @@ -117,7 +108,7 @@ class ScheduledExecutionFromRdJobUpdater { } } - static void updateJobOption(Option opt, RdOption rdo) { + static void updateJobOption(Option opt, OptionData rdo) { opt.sortIndex = rdo.sortIndex opt.name = rdo.name opt.description = rdo.description @@ -146,14 +137,14 @@ class ScheduledExecutionFromRdJobUpdater { opt.optionValues = rdo.optionValues ? new ArrayList(rdo.optionValues) : null } - static void updateLogConfig(ScheduledExecution se, RdLogConfig logConfig) { + static void updateLogConfig(ScheduledExecution se, LogConfig logConfig) { se.loglevel = logConfig.loglevel se.logOutputThreshold = logConfig.logOutputThreshold se.logOutputThresholdAction = logConfig.logOutputThresholdAction se.logOutputThresholdStatus = logConfig.logOutputThresholdStatus } - static void updateNodeConfig(ScheduledExecution se, RdNodeConfig nodeConfig) { + static void updateNodeConfig(ScheduledExecution se, NodeConfig nodeConfig) { se.nodeInclude = nodeConfig.nodeInclude se.nodeIncludeName = nodeConfig.nodeIncludeName se.nodeIncludeTags = nodeConfig.nodeIncludeTags @@ -183,7 +174,7 @@ class ScheduledExecutionFromRdJobUpdater { se.excludeFilterUncheck = nodeConfig.excludeFilterUncheck } - static void updateSchedule(ScheduledExecution se, RdSchedule schedule) { + static void updateSchedule(ScheduledExecution se, ScheduleData schedule) { se.year = schedule?.year se.month = schedule?.month se.dayOfWeek = schedule?.dayOfWeek diff --git a/rundeckapp/src/main/groovy/org/rundeck/app/data/job/converters/WorkflowUpdater.groovy b/rundeckapp/src/main/groovy/org/rundeck/app/data/job/converters/WorkflowUpdater.groovy index afffa9b407e..5f64cea115a 100644 --- a/rundeckapp/src/main/groovy/org/rundeck/app/data/job/converters/WorkflowUpdater.groovy +++ b/rundeckapp/src/main/groovy/org/rundeck/app/data/job/converters/WorkflowUpdater.groovy @@ -1,18 +1,17 @@ package org.rundeck.app.data.job.converters +import org.rundeck.app.data.model.v1.job.workflow.WorkflowData +import org.rundeck.app.data.model.v1.job.workflow.WorkflowStepData import rundeck.CommandExec import rundeck.JobExec import rundeck.PluginStep -import rundeck.ScheduledExecution import rundeck.Workflow import rundeck.WorkflowStep import rundeck.data.constants.WorkflowStepConstants -import rundeck.data.job.RdWorkflow -import rundeck.data.job.RdWorkflowStep class WorkflowUpdater { - static void updateWorkflow(Workflow wkf, RdWorkflow rdw) { + static void updateWorkflow(Workflow wkf, WorkflowData rdw) { if(!wkf || !rdw) return wkf.threadcount = rdw.threadcount wkf.keepgoing = rdw.keepgoing @@ -34,13 +33,13 @@ class WorkflowUpdater { } - static WorkflowStep createWorkflowStep(RdWorkflowStep step) { + static WorkflowStep createWorkflowStep(WorkflowStepData step) { if(step.pluginType == WorkflowStepConstants.TYPE_JOB_REF) return new JobExec() else if(step.pluginType.startsWith("builtin-")) return new CommandExec() return new PluginStep() } - static void updateWorkflowStep(WorkflowStep step, RdWorkflowStep rdstep) { + static void updateWorkflowStep(WorkflowStep step, WorkflowStepData rdstep) { if(step instanceof JobExec) { def jstep = (JobExec)step JobExec.updateFromMap(jstep, rdstep.configuration) diff --git a/rundeckapp/src/main/groovy/org/rundeck/app/data/providers/GormJobDataProvider.groovy b/rundeckapp/src/main/groovy/org/rundeck/app/data/providers/GormJobDataProvider.groovy index 61af40ce8b2..ff1315c9eb6 100644 --- a/rundeckapp/src/main/groovy/org/rundeck/app/data/providers/GormJobDataProvider.groovy +++ b/rundeckapp/src/main/groovy/org/rundeck/app/data/providers/GormJobDataProvider.groovy @@ -1,51 +1,23 @@ package org.rundeck.app.data.providers -import com.dtolabs.rundeck.core.authorization.UserAndRolesAuthContext -import com.dtolabs.rundeck.core.common.INodeSet -import com.dtolabs.rundeck.core.jobs.JobLifecycleComponentException -import com.dtolabs.rundeck.core.jobs.JobOption -import com.dtolabs.rundeck.core.jobs.options.JobOptionConfigData -import com.dtolabs.rundeck.plugins.jobs.JobOptionImpl -import com.dtolabs.rundeck.plugins.jobs.JobPersistEventImpl -import com.fasterxml.jackson.databind.ObjectMapper import grails.compiler.GrailsCompileStatic -import grails.events.annotation.Publisher import grails.gorm.transactions.Transactional import groovy.transform.CompileDynamic import groovy.transform.CompileStatic import groovy.transform.TypeCheckingMode import groovy.util.logging.Log4j2 -import org.rundeck.app.authorization.AppAuthContextProcessor -import org.rundeck.app.components.RundeckJobDefinitionManager -import org.rundeck.app.components.jobs.ImportedJob -import org.rundeck.app.data.model.v1.job.JobDataSummary -import rundeck.data.job.reference.JobReferenceImpl -import rundeck.data.job.reference.JobRevReferenceImpl -import rundeck.data.validation.exception.DataValidationException -import org.rundeck.app.data.job.schedule.DefaultJobDataChangeDetector -import org.rundeck.app.data.model.v1.DeletionResult -import org.rundeck.app.data.model.v1.job.component.JobComponentData -import org.rundeck.app.events.LogJobChangeEvent -import org.rundeck.app.job.component.JobComponentDataImportExport -import org.rundeck.app.jobs.options.JobOptionConfigPluginAttributes -import org.rundeck.core.auth.AuthConstants -import org.springframework.web.context.request.RequestContextHolder -import rundeck.data.job.RdJob import org.rundeck.app.data.job.converters.ScheduledExecutionFromRdJobUpdater import org.rundeck.app.data.job.converters.ScheduledExecutionToJobConverter +import org.rundeck.app.data.model.v1.DeletionResult import org.rundeck.app.data.model.v1.job.JobData +import org.rundeck.app.data.model.v1.job.JobDataSummary import org.rundeck.app.data.providers.v1.job.JobDataProvider import org.rundeck.spi.data.DataAccessException import org.springframework.beans.factory.annotation.Autowired +import org.springframework.web.context.request.RequestContextHolder import rundeck.ScheduledExecution -import rundeck.data.job.RdOption import rundeck.services.FrameworkService -import rundeck.services.JobLifecycleComponentService - -import rundeck.services.JobSchedulerService -import rundeck.services.JobSchedulesService import rundeck.services.ScheduledExecutionService -import rundeck.services.audit.JobUpdateAuditEvent import rundeck.services.data.ScheduledExecutionDataService import javax.persistence.EntityNotFoundException @@ -58,19 +30,9 @@ class GormJobDataProvider extends GormJobQueryProvider implements JobDataProvide @Autowired ScheduledExecutionDataService scheduledExecutionDataService @Autowired - RundeckJobDefinitionManager rundeckJobDefinitionManager - @Autowired FrameworkService frameworkService @Autowired - JobSchedulesService jobSchedulesService - @Autowired - JobSchedulerService jobSchedulerService - @Autowired - AppAuthContextProcessor rundeckAuthContextProcessor - @Autowired ScheduledExecutionService scheduledExecutionService - @Autowired - JobLifecycleComponentService jobLifecycleComponentService @Override JobData get(Serializable id) { @@ -118,44 +80,13 @@ class GormJobDataProvider extends GormJobQueryProvider implements JobDataProvide @CompileStatic(TypeCheckingMode.SKIP) @Override JobData save(JobData job) throws DataAccessException { - def session = getSession() - def authCtx = frameworkService.userAuthContext(session) ScheduledExecution se = job.uuid ? scheduledExecutionDataService.findByUuid(job.uuid) : null - boolean isnew = false if(!se) { - isnew = true se = new ScheduledExecution() } - RdJob rdJob = (RdJob)job - if(!rdJob.validate()) { - throw new DataValidationException(rdJob) - } - LogJobChangeEvent logEvent = new LogJobChangeEvent(isnew ? 'create' : 'update','GormJobDataProvider.save', session.user) - JobChangeData jobChangeData = detectJobChanges(se, rdJob, logEvent) - runComponentBeforeSave(authCtx.username, rdJob) - ScheduledExecutionFromRdJobUpdater.update(se, rdJob) - authorizeEditAndUpdateJobUserAndRoles(authCtx, se, rdJob) - def importedJob = validateComponents(se, rdJob) - if(rdJob.errors.hasErrors()) throw new DataValidationException(rdJob) - - rundeckJobDefinitionManager.persistComponents(importedJob,authCtx) + ScheduledExecutionFromRdJobUpdater.update(se, job) def saved = se.save(failOnError:true, flush: true) - rundeckJobDefinitionManager.waspersisted(importedJob, authCtx) - rescheduleJob(saved, jobChangeData) - publishJobUpdateAuditEvent(se, isnew) - publishLogJobChangeEvent(saved, logEvent) - return ScheduledExecutionToJobConverter.convert(saved) - } - - @Publisher('log.job.change.event') - LogJobChangeEvent publishLogJobChangeEvent(ScheduledExecution se, LogJobChangeEvent event) { - event.jobData = se - return event - } - - @Publisher('audit.job.update') - JobUpdateAuditEvent publishJobUpdateAuditEvent(ScheduledExecution se, boolean isnew) { - return new JobUpdateAuditEvent(jobUuid: se.uuid, project: se.project, fullName: se.generateFullName(), isnew: isnew) + return saved } @CompileStatic(TypeCheckingMode.SKIP) @@ -181,189 +112,4 @@ class GormJobDataProvider extends GormJobQueryProvider implements JobDataProvide return scheduledExecutionService.deleteScheduledExecution(se,false,authCtx,authCtx.username) } - void runComponentBeforeSave(String username, RdJob rdJob) { - INodeSet nodeSet = scheduledExecutionService.getNodes(rdJob,null) - JobPersistEventImpl jobPersistEvent = new JobPersistEventImpl( - rdJob.jobName, - rdJob.project, - convertToJobOptions(rdJob.optionSet), - nodeSet, - username, - rdJob.nodeConfig.filter - ) - def jobEventStatus - try { - jobEventStatus = jobLifecycleComponentService?.beforeJobSave(rdJob.project, jobPersistEvent) - } catch (JobLifecycleComponentException exception) { - exception.printStackTrace() - log.debug("JobLifecycle error: " + exception.message, exception) - rdJob.errors.reject( - 'scheduledExecution.plugin.error.message', - ['Job Lifecycle: ' + exception.message].toArray(), - "A Plugin returned an error: " + exception.message - ) - return - } - if(jobEventStatus?.isUseNewValues()) { - rdJob.optionSet = jobEventStatus.getOptions() - .collect {opt -> RdOption.convertFromJobOption(opt, rdJob.optionSet?.find { it.name == opt.name })} as SortedSet - rdJob.validate() - } - } - - ImportedJob validateComponents(ScheduledExecution se, RdJob rdJob) { - def associations = [:] as Map - rundeckJobDefinitionManager.jobDefinitionComponents.each{ k, val -> - if(!(val instanceof JobComponentDataImportExport)) { - log.warn("Job component {} cannot be imported to the job data because no importer is defined", k) - return - } - JobComponentDataImportExport importer = (JobComponentDataImportExport)val - if(!rdJob.components.containsKey(importer.componentKey)) return - def data = importer.importFromJobData(rdJob) - if(data) associations[k] = data - } - def importedJob = RundeckJobDefinitionManager.importedJob(se,associations) - def updatedJob = rundeckJobDefinitionManager.updateJob(se, importedJob, [:]) - validateComponentsExist(rdJob) - def rptSet = rundeckJobDefinitionManager.validateImportedJob(updatedJob) - rptSet.validations.each { componentName, rpt -> - rpt.errors.each { k, v -> - rdJob.errors.rejectValue("components", - "jobData.components.invalidconfiguration", - [componentName, k, v] as Object[], - 'Job Component: {0} invalid config: {1} : {2}') - } - } - return updatedJob - } - - void validateComponentsExist(RdJob rdJob) { - def jobDefinitionComponentKeys = rundeckJobDefinitionManager.jobDefinitionComponents.values().findAll{it instanceof JobComponentDataImportExport }.collect { ((JobComponentDataImportExport)it).componentKey } - rdJob.components.each { String componentName, JobComponentData value -> - if (!jobDefinitionComponentKeys.contains(componentName)) { - rdJob.errors.rejectValue("components", - "jobData.components.notfound", - [componentName] as Object[], - 'Job Component of type: {0} could not be found') - } - } - } - - JobChangeData detectJobChanges(ScheduledExecution se, RdJob rdJob, LogJobChangeEvent logEvent) { - JobChangeData jobChangeData = new JobChangeData() - if(!se.id) return jobChangeData - String oldjobname = se.generateJobScheduledName() - String oldjobgroup = se.generateJobGroupName() - jobChangeData.isScheduled = jobSchedulesService.isScheduled(se.uuid) - DefaultJobDataChangeDetector detector = new DefaultJobDataChangeDetector( - localScheduled: se.scheduled, - originalCron: se.generateCrontabExression(), - originalSchedule: se.scheduleEnabled, - originalExecution: se.executionEnabled, - originalTz: se.timeZone, - originalRef: new JobRevReferenceImpl( - id: se.extid, - jobName: se.jobName, - groupPath: se.groupPath, - project: se.project, - version: se.version - ) - ) - jobChangeData.renamed = detector.wasRenamed(rdJob) - - if(jobChangeData.renamed){ - logEvent.changeinfo.rename = true - logEvent.changeinfo.origName = oldjobname - logEvent.changeinfo.origGroup = oldjobgroup - jobChangeData.scheduledJobName = oldjobname - jobChangeData.scheduledGroupPath = oldjobgroup - } - - boolean schedulingWasChanged = detector.schedulingWasChanged(se) - if(frameworkService.isClusterModeEnabled()){ - if (schedulingWasChanged) { - JobReferenceImpl jobReference = se.asReference() as JobReferenceImpl - jobReference.setOriginalQuartzJobName(oldjobname) - jobReference.setOriginalQuartzGroupName(oldjobgroup) - jobChangeData.scheduleOwnerModified = jobSchedulerService.updateScheduleOwner(jobReference) - if (jobChangeData.scheduleOwnerModified) { - rdJob.serverNodeUUID = frameworkService.serverUUID - } - } - if (!rdJob.serverNodeUUID) { - rdJob.serverNodeUUID = frameworkService.serverUUID - } - } - - jobChangeData - } - - @CompileStatic(TypeCheckingMode.SKIP) - void authorizeEditAndUpdateJobUserAndRoles(UserAndRolesAuthContext authContext, ScheduledExecution se, RdJob rdJob) { - def authAction = se.id ? AuthConstants.ACTION_UPDATE : AuthConstants.ACTION_CREATE - se.user = authContext.username - se.userRoles = authContext.roles as List - if (!rundeckAuthContextProcessor.authorizeProjectJobAll(authContext, se, [authAction], se.project)) { - rdJob.errors.rejectValue('jobName', 'ScheduledExecution.jobName.unauthorized', [authAction, rdJob.jobName].toArray(), 'Unauthorized action: {0} for value: {1}') - rdJob.errors.rejectValue('groupPath', 'ScheduledExecution.groupPath.unauthorized', [ authAction, rdJob.groupPath].toArray(), 'Unauthorized action: {0} for value: {1}') - return - } - } - - void rescheduleJob(ScheduledExecution se, JobChangeData jobChangeData) { - scheduledExecutionService.rescheduleJob(se, jobChangeData.isScheduled, - jobChangeData.scheduledJobName, jobChangeData.scheduledGroupPath, false, - !jobChangeData.schedulingWasChanged || !jobChangeData.scheduleOwnerModified) - } - - SortedSet convertToJobOptions(SortedSet rdOptions) { - def opts = new TreeSet() - if(!rdOptions) return opts - - opts.addAll(rdOptions.collect {opt -> - JobOptionConfigData jobOptionConfigData= new JobOptionConfigData() - jobOptionConfigData.addConfig(new JobOptionConfigPluginAttributes(opt.configMap)) - - JobOptionImpl.builder() - .name(opt.name) - .description(opt.description) - .defaultValue(opt.defaultValue) - .delimiter(opt.delimiter) - .defaultStoragePath(opt.defaultStoragePath) - .isDate(opt.isDate) - .dateFormat(opt.dateFormat) - .regex(opt.regex) - .enforced(opt.enforced) - .hidden(opt.hidden) - .optionType(opt.optionType) - .label(opt.label) - .required(opt.required) - .realValuesUrl(opt.realValuesUrl) - .sortIndex(opt.sortIndex) - .optionValues(opt.optionValues) - .optionValuesPluginType(opt.optionValuesPluginType) - .secureExposed(opt.secureExposed) - .secureInput(opt.secureInput) - .configData(jobOptionConfigData) - .multivalueAllSelected(opt.multivalueAllSelected) - .multivalued(opt.multivalued) - .sortValues(opt.sortValues) - .valuesListDelimiter(opt.valuesListDelimiter) - .valuesList(opt.valuesList) - .build() - }) - opts - } - - static ObjectMapper mapper = new ObjectMapper() - - static class JobChangeData { - String scheduledJobName - String scheduledGroupPath - boolean isScheduled - boolean renamed - boolean scheduleOwnerModified - boolean schedulingWasChanged - } } diff --git a/rundeckapp/src/test/groovy/org/rundeck/app/data/providers/GormJobDataProviderSpec.groovy b/rundeckapp/src/test/groovy/org/rundeck/app/data/providers/GormJobDataProviderSpec.groovy index feb44483828..420acb1abf6 100644 --- a/rundeckapp/src/test/groovy/org/rundeck/app/data/providers/GormJobDataProviderSpec.groovy +++ b/rundeckapp/src/test/groovy/org/rundeck/app/data/providers/GormJobDataProviderSpec.groovy @@ -1,20 +1,9 @@ package org.rundeck.app.data.providers import com.dtolabs.rundeck.core.authorization.UserAndRolesAuthContext -import com.dtolabs.rundeck.core.common.INodeSet -import com.dtolabs.rundeck.core.common.IRundeckProject -import com.dtolabs.rundeck.core.jobs.JobLifecycleStatus -import com.dtolabs.rundeck.core.jobs.JobOption -import com.dtolabs.rundeck.core.plugins.configuration.Validator import grails.testing.gorm.DataTest import org.grails.spring.beans.factory.InstanceFactoryBean -import org.rundeck.app.authorization.AppAuthContextProcessor import org.rundeck.app.components.RundeckJobDefinitionManager -import org.rundeck.app.components.jobs.ImportedJob -import org.rundeck.app.components.jobs.JobDefinitionComponent -import org.rundeck.app.data.model.v1.job.component.JobComponentData -import org.rundeck.app.events.LogJobChangeEvent -import org.springframework.validation.Errors import rundeck.CommandExec import rundeck.ScheduledExecution import rundeck.Workflow @@ -25,8 +14,6 @@ import rundeck.data.job.RdNodeConfig import rundeck.data.job.RdWorkflow import rundeck.data.job.RdWorkflowStep import rundeck.services.FrameworkService -import rundeck.services.JobLifecycleComponentService -import rundeck.services.JobSchedulesService import rundeck.services.ScheduledExecutionService import rundeck.services.data.ScheduledExecutionDataService import spock.lang.Specification @@ -37,12 +24,6 @@ import javax.servlet.http.HttpSession class GormJobDataProviderSpec extends Specification implements DataTest { GormJobDataProvider provider = new GormJobDataProvider() - Closure doWithSpring() { - { -> - rundeckJobDefinitionManager(InstanceFactoryBean, Mock(RundeckJobDefinitionManager)) - } - } - def setup() { def httpSession = Mock(HttpSession) { getSubject() >> new Subject() @@ -59,33 +40,6 @@ class GormJobDataProviderSpec extends Specification implements DataTest { def "Save"() { given: RdJob rdJob = mockedJob() - 1 * rdJob.validate() >> true - 1 * rdJob.getErrors() >> Mock(Errors) { - hasErrors() >> false - } - provider.frameworkService = Mock(FrameworkService) { - 1 * userAuthContext(_) >> Mock(UserAndRolesAuthContext) { - getUsername() >> "user" - getRoles() >> new TreeSet<>(["r1","r2"]) - } - existsFrameworkProject("one") >> true - getFrameworkProject("one") >> Mock(IRundeckProject) - - } - provider.rundeckAuthContextProcessor = Mock(AppAuthContextProcessor) { - 1 * authorizeProjectJobAll(_,_,_,_) >> true - } - - provider.scheduledExecutionService = Mock(ScheduledExecutionService) { - 1 * rescheduleJob(_,_,_,_,_,_) >> null - } - provider.rundeckJobDefinitionManager = Mock(RundeckJobDefinitionManager) { - 2 * getJobDefinitionComponents() >> [:] - 1 * validateImportedJob(_) >> Mock(Validator.ReportSet) { - getValidations() >> [:] - } - 1 * updateJob(_,_,_) >> null - } when: def saved = provider.save(rdJob) @@ -112,122 +66,6 @@ class GormJobDataProviderSpec extends Specification implements DataTest { } - def "RunComponentBeforeSave"() { - given: - provider.scheduledExecutionService = Mock(ScheduledExecutionService) { - 1 * getNodes(_,_) >> Mock(INodeSet) - } - provider.jobLifecycleComponentService = Mock(JobLifecycleComponentService) { - 1 * beforeJobSave(_,_) >> Mock(JobLifecycleStatus) { - isUseNewValues() >> true - getOptions() >> new TreeSet() - } - } - RdJob job = mockedJob() - - when: - provider.runComponentBeforeSave("user", job) - - then: - 1 * job.validate() - - } - - def "ValidateComponents"() { - given: - RdJob job = new RdJob() - ScheduledExecution se = new ScheduledExecution() - provider.rundeckJobDefinitionManager = Mock(RundeckJobDefinitionManager) { - 2 * getJobDefinitionComponents() >> [:] - 1 * updateJob(_,_,_) >> Mock(ImportedJob) - 1 * validateImportedJob(_) >> Mock(Validator.ReportSet) { - 1 * getValidations() >> validations - } - } - - when: - provider.validateComponents(se, job) - - then: - job.errors.errorCount == errCount - - where: - errCount | validations - 0 | [:] - 1 | ["component1":new Validator.Report(errors: ["err1":"validation error"])] - } - - def "ValidateComponentsExist"() { - given: - RdJob job = mockedJob() - - provider.rundeckJobDefinitionManager = Mock(RundeckJobDefinitionManager) { - 1 * getJobDefinitionComponents() >> ["component1": [:] as JobDefinitionComponent] - } - - when: - provider.validateComponentsExist(job) - - then: - - 1* job.getComponents() >> ["job-queue":[:] as JobComponentData] - 1* job.getErrors() >> Mock(Errors) { - 1 * rejectValue("components","jobData.components.notfound",_,_) - } - - - } - - def "DetectJobChanges"() { - given: - String uuid = "uuid" - RdJob job = new RdJob(uuid: uuid, jobName: newjob, project:"one") - ScheduledExecution se = new ScheduledExecution( uuid: uuid, jobName: oldjob, project:"one") - se.id = 1L - LogJobChangeEvent event = new LogJobChangeEvent() - provider.jobSchedulesService = Mock(JobSchedulesService) { - isScheduled(_) >> true - } - provider.frameworkService = Mock(FrameworkService) { - isClusterModeEnabled() >> false - } - - when: - def actual = provider.detectJobChanges(se, job, event) - - then: - actual.scheduledJobName == expectedJobName - actual.renamed == renamed - event.changeinfo.origName == expectedJobName - if(renamed) event.changeinfo.rename - - where: - expectedJobName | renamed | oldjob | newjob - "1:oldjob" | true | "oldjob" | "newjob" - null | false | "job1" | "job1" - - } - - def "AuthorizeEditAndUpdateJobUserAndRoles"() { - given: - RdJob job = new RdJob() - def authCtx = Mock(UserAndRolesAuthContext) { - getUsername() >> "user" - getRoles() >> ["r1","r2"] - } - provider.rundeckAuthContextProcessor = Mock(AppAuthContextProcessor) - ScheduledExecution se = new ScheduledExecution() - - when: - provider.authorizeEditAndUpdateJobUserAndRoles(authCtx, se, job) - - then: - 1 * provider.rundeckAuthContextProcessor.authorizeProjectJobAll(_,_,_,_) >> false - job.errors.errorCount == 2 - se.user == "user" - se.userRoles == ["r1","r2"] - } - def mockedJob() { return Mock(RdJob) { diff --git a/rundeckapp/src/test/groovy/rundeck/services/RdJobServiceSpec.groovy b/rundeckapp/src/test/groovy/rundeck/services/RdJobServiceSpec.groovy new file mode 100644 index 00000000000..5b31e62aeec --- /dev/null +++ b/rundeckapp/src/test/groovy/rundeck/services/RdJobServiceSpec.groovy @@ -0,0 +1,229 @@ +package rundeck.services + +import com.dtolabs.rundeck.core.authorization.UserAndRolesAuthContext +import com.dtolabs.rundeck.core.common.INodeSet +import com.dtolabs.rundeck.core.common.IRundeckProject +import com.dtolabs.rundeck.core.jobs.JobLifecycleStatus +import com.dtolabs.rundeck.core.jobs.JobOption +import com.dtolabs.rundeck.core.plugins.configuration.Validator +import grails.testing.gorm.DataTest +import org.grails.spring.beans.factory.InstanceFactoryBean +import org.rundeck.app.authorization.AppAuthContextProcessor +import org.rundeck.app.components.RundeckJobDefinitionManager +import org.rundeck.app.components.jobs.ImportedJob +import org.rundeck.app.components.jobs.JobDefinitionComponent +import org.rundeck.app.data.model.v1.job.component.JobComponentData +import org.rundeck.app.data.providers.GormJobDataProvider +import org.rundeck.app.events.LogJobChangeEvent +import org.springframework.validation.Errors +import rundeck.CommandExec +import rundeck.ScheduledExecution +import rundeck.Workflow +import rundeck.WorkflowStep +import rundeck.data.job.RdJob +import rundeck.data.job.RdLogConfig +import rundeck.data.job.RdNodeConfig +import rundeck.data.job.RdWorkflow +import rundeck.data.job.RdWorkflowStep +import rundeck.services.data.ScheduledExecutionDataService +import spock.lang.Specification + +import javax.security.auth.Subject +import javax.servlet.http.HttpSession + +class RdJobServiceSpec extends Specification implements DataTest { + RdJobService service + GormJobDataProvider provider = new GormJobDataProvider() + + Closure doWithSpring() { + { -> + rundeckJobDefinitionManager(InstanceFactoryBean, Mock(RundeckJobDefinitionManager)) + } + } + + void setup() { + def httpSession = Mock(HttpSession) { + getSubject() >> new Subject() + getAttribute("user") >> "user" + } + provider.metaClass.getSession = { + httpSession + } + mockDomains(ScheduledExecution, Workflow, WorkflowStep, CommandExec) + mockDataService(ScheduledExecutionDataService) + provider.scheduledExecutionDataService = applicationContext.getBean(ScheduledExecutionDataService) + service = new RdJobService() + service.scheduledExecutionDataService = applicationContext.getBean(ScheduledExecutionDataService) + service.jobDataProvider = provider + service.metaClass.getSession = { + httpSession + } + } + + def "SaveJob"() { + + given: + RdJob rdJob = mockedJob() + 1 * rdJob.validate() >> true + 1 * rdJob.getErrors() >> Mock(Errors) { + hasErrors() >> false + } + service.frameworkService = Mock(FrameworkService) { + 1 * userAuthContext(_) >> Mock(UserAndRolesAuthContext) { + getUsername() >> "user" + getRoles() >> new TreeSet<>(["r1","r2"]) + } + existsFrameworkProject("one") >> true + getFrameworkProject("one") >> Mock(IRundeckProject) + + } + service.rundeckAuthContextProcessor = Mock(AppAuthContextProcessor) { + 1 * authorizeProjectJobAll(_,_,_,_) >> true + } + + service.scheduledExecutionService = Mock(ScheduledExecutionService) { + 1 * rescheduleJob(_,_,_,_,_,_) >> null + } + service.rundeckJobDefinitionManager = Mock(RundeckJobDefinitionManager) { + 2 * getJobDefinitionComponents() >> [:] + 1 * validateImportedJob(_) >> Mock(Validator.ReportSet) { + getValidations() >> [:] + } + 1 * updateJob(_,_,_) >> null + } + + when: + def saved = service.saveJob(rdJob) + then: + saved.uuid + } + + def "RunComponentBeforeSave"() { + given: + service.scheduledExecutionService = Mock(ScheduledExecutionService) { + 1 * getNodes(_,_) >> Mock(INodeSet) + } + service.jobLifecycleComponentService = Mock(JobLifecycleComponentService) { + 1 * beforeJobSave(_,_) >> Mock(JobLifecycleStatus) { + isUseNewValues() >> true + getOptions() >> new TreeSet() + } + } + RdJob job = mockedJob() + + when: + service.runComponentBeforeSave("user", job) + + then: + 1 * job.validate() + + } + + def "ValidateComponents"() { + given: + RdJob job = new RdJob() + ScheduledExecution se = new ScheduledExecution() + service.rundeckJobDefinitionManager = Mock(RundeckJobDefinitionManager) { + 2 * getJobDefinitionComponents() >> [:] + 1 * updateJob(_,_,_) >> Mock(ImportedJob) + 1 * validateImportedJob(_) >> Mock(Validator.ReportSet) { + 1 * getValidations() >> validations + } + } + + when: + service.validateComponents(se, job) + + then: + job.errors.errorCount == errCount + + where: + errCount | validations + 0 | [:] + 1 | ["component1":new Validator.Report(errors: ["err1":"validation error"])] + } + + def "ValidateComponentsExist"() { + given: + RdJob job = mockedJob() + + service.rundeckJobDefinitionManager = Mock(RundeckJobDefinitionManager) { + 1 * getJobDefinitionComponents() >> ["component1": [:] as JobDefinitionComponent] + } + + when: + service.validateComponentsExist(job) + + then: + + 1* job.getComponents() >> ["job-queue":[:] as JobComponentData] + 1* job.getErrors() >> Mock(Errors) { + 1 * rejectValue("components","jobData.components.notfound",_,_) + } + + + } + + def "DetectJobChanges"() { + given: + String uuid = "uuid" + RdJob job = new RdJob(uuid: uuid, jobName: newjob, project:"one") + ScheduledExecution se = new ScheduledExecution( uuid: uuid, jobName: oldjob, project:"one") + se.id = 1L + LogJobChangeEvent event = new LogJobChangeEvent() + service.jobSchedulesService = Mock(JobSchedulesService) { + isScheduled(_) >> true + } + service.frameworkService = Mock(FrameworkService) { + isClusterModeEnabled() >> false + } + + when: + def actual = service.detectJobChanges(se, job, event) + + then: + actual.scheduledJobName == expectedJobName + actual.renamed == renamed + event.changeinfo.origName == expectedJobName + if(renamed) event.changeinfo.rename + + where: + expectedJobName | renamed | oldjob | newjob + "1:oldjob" | true | "oldjob" | "newjob" + null | false | "job1" | "job1" + + } + + def "AuthorizeEditAndUpdateJobUserAndRoles"() { + given: + RdJob job = new RdJob() + def authCtx = Mock(UserAndRolesAuthContext) { + getUsername() >> "user" + getRoles() >> ["r1","r2"] + } + service.rundeckAuthContextProcessor = Mock(AppAuthContextProcessor) + ScheduledExecution se = new ScheduledExecution() + + when: + service.authorizeEditAndUpdateJobUserAndRoles(authCtx, se, job) + + then: + 1 * service.rundeckAuthContextProcessor.authorizeProjectJobAll(_,_,_,_) >> false + job.errors.errorCount == 2 + se.user == "user" + se.userRoles == ["r1","r2"] + } + + + def mockedJob() { + return Mock(RdJob) { + getJobName() >> "job1" + getProject() >> "one" + getScheduled() >> false + getNodeConfig() >> new RdNodeConfig() + getLogConfig() >> new RdLogConfig() + getComponents() >> [:] + getWorkflow() >> new RdWorkflow(steps: [new RdWorkflowStep(nodeStep: false,pluginType: "builtin-command",configuration: [exec:"echo hello"])]) + } + } +}