diff --git a/api/cmd/portainer/main.go b/api/cmd/portainer/main.go index 7e5b4f581..cd1106159 100644 --- a/api/cmd/portainer/main.go +++ b/api/cmd/portainer/main.go @@ -136,6 +136,7 @@ func loadSnapshotSystemSchedule(jobScheduler portainer.JobScheduler, snapshotter ID: portainer.ScheduleID(scheduleService.GetNextIdentifier()), Name: "system_snapshot", CronExpression: "@every " + *flags.SnapshotInterval, + Recurring: true, JobType: portainer.SnapshotJobType, SnapshotJob: snapshotJob, Created: time.Now().Unix(), @@ -174,6 +175,7 @@ func loadEndpointSyncSystemSchedule(jobScheduler portainer.JobScheduler, schedul ID: portainer.ScheduleID(scheduleService.GetNextIdentifier()), Name: "system_endpointsync", CronExpression: "@every " + *flags.SyncInterval, + Recurring: true, JobType: portainer.EndpointSyncJobType, EndpointSyncJob: endpointSyncJob, Created: time.Now().Unix(), diff --git a/api/cron/job_script_execution.go b/api/cron/job_script_execution.go index 6f984e8fd..2143a83f5 100644 --- a/api/cron/job_script_execution.go +++ b/api/cron/job_script_execution.go @@ -9,8 +9,9 @@ import ( // ScriptExecutionJobRunner is used to run a ScriptExecutionJob type ScriptExecutionJobRunner struct { - schedule *portainer.Schedule - context *ScriptExecutionJobContext + schedule *portainer.Schedule + context *ScriptExecutionJobContext + executedOnce bool } // ScriptExecutionJobContext represents the context of execution of a ScriptExecutionJob @@ -32,8 +33,9 @@ func NewScriptExecutionJobContext(jobService portainer.JobService, endpointServi // NewScriptExecutionJobRunner returns a new runner that can be scheduled func NewScriptExecutionJobRunner(schedule *portainer.Schedule, context *ScriptExecutionJobContext) *ScriptExecutionJobRunner { return &ScriptExecutionJobRunner{ - schedule: schedule, - context: context, + schedule: schedule, + context: context, + executedOnce: false, } } @@ -41,6 +43,11 @@ func NewScriptExecutionJobRunner(schedule *portainer.Schedule, context *ScriptEx // It will iterate through all the endpoints specified in the context to // execute the script associated to the job. func (runner *ScriptExecutionJobRunner) Run() { + if !runner.schedule.Recurring && runner.executedOnce { + return + } + runner.executedOnce = true + scriptFile, err := runner.context.fileService.GetFileContent(runner.schedule.ScriptExecutionJob.ScriptPath) if err != nil { log.Printf("scheduled job error (script execution). Unable to retrieve script file (err=%s)\n", err) diff --git a/api/http/handler/schedules/schedule_create.go b/api/http/handler/schedules/schedule_create.go index 893ec49f3..5c0ecbdbc 100644 --- a/api/http/handler/schedules/schedule_create.go +++ b/api/http/handler/schedules/schedule_create.go @@ -18,6 +18,7 @@ type scheduleCreateFromFilePayload struct { Name string Image string CronExpression string + Recurring bool Endpoints []portainer.EndpointID File []byte RetryCount int @@ -27,6 +28,7 @@ type scheduleCreateFromFilePayload struct { type scheduleCreateFromFileContentPayload struct { Name string CronExpression string + Recurring bool Image string Endpoints []portainer.EndpointID FileContent string @@ -174,9 +176,8 @@ func (handler *Handler) createScheduleObjectFromFilePayload(payload *scheduleCre scheduleIdentifier := portainer.ScheduleID(handler.ScheduleService.GetNextIdentifier()) job := &portainer.ScriptExecutionJob{ - Endpoints: payload.Endpoints, - Image: payload.Image, - // ScheduleID: scheduleIdentifier, + Endpoints: payload.Endpoints, + Image: payload.Image, RetryCount: payload.RetryCount, RetryInterval: payload.RetryInterval, } @@ -185,6 +186,7 @@ func (handler *Handler) createScheduleObjectFromFilePayload(payload *scheduleCre ID: scheduleIdentifier, Name: payload.Name, CronExpression: payload.CronExpression, + Recurring: payload.Recurring, JobType: portainer.ScriptExecutionJobType, ScriptExecutionJob: job, Created: time.Now().Unix(), @@ -197,9 +199,8 @@ func (handler *Handler) createScheduleObjectFromFileContentPayload(payload *sche scheduleIdentifier := portainer.ScheduleID(handler.ScheduleService.GetNextIdentifier()) job := &portainer.ScriptExecutionJob{ - Endpoints: payload.Endpoints, - Image: payload.Image, - // ScheduleID: scheduleIdentifier, + Endpoints: payload.Endpoints, + Image: payload.Image, RetryCount: payload.RetryCount, RetryInterval: payload.RetryInterval, } @@ -208,6 +209,7 @@ func (handler *Handler) createScheduleObjectFromFileContentPayload(payload *sche ID: scheduleIdentifier, Name: payload.Name, CronExpression: payload.CronExpression, + Recurring: payload.Recurring, JobType: portainer.ScriptExecutionJobType, ScriptExecutionJob: job, Created: time.Now().Unix(), diff --git a/api/http/handler/schedules/schedule_update.go b/api/http/handler/schedules/schedule_update.go index 7e741631d..0edfd0dde 100644 --- a/api/http/handler/schedules/schedule_update.go +++ b/api/http/handler/schedules/schedule_update.go @@ -17,6 +17,7 @@ type scheduleUpdatePayload struct { Name *string Image *string CronExpression *string + Recurring *bool Endpoints []portainer.EndpointID FileContent *string RetryCount *int @@ -101,6 +102,11 @@ func updateSchedule(schedule *portainer.Schedule, payload *scheduleUpdatePayload updateJobSchedule = true } + if payload.Recurring != nil { + schedule.Recurring = *payload.Recurring + updateJobSchedule = true + } + if payload.Image != nil { schedule.ScriptExecutionJob.Image = *payload.Image updateJobSchedule = true diff --git a/api/portainer.go b/api/portainer.go index 1c6d103c6..ad7891c9d 100644 --- a/api/portainer.go +++ b/api/portainer.go @@ -244,11 +244,13 @@ type ( // Schedule represents a scheduled job. // It only contains a pointer to one of the JobRunner implementations - // based on the JobType + // based on the JobType. + // NOTE: The Recurring option is only used by ScriptExecutionJob at the moment Schedule struct { ID ScheduleID `json:"Id"` Name string CronExpression string + Recurring bool Created int64 JobType JobType ScriptExecutionJob *ScriptExecutionJob diff --git a/app/__module.js b/app/__module.js index 00be9300d..429e2acc0 100644 --- a/app/__module.js +++ b/app/__module.js @@ -23,4 +23,6 @@ angular.module('portainer', [ 'portainer.azure', 'portainer.docker', 'extension.storidge', - 'rzModule']); + 'rzModule', + 'moment-picker' + ]); diff --git a/app/portainer/components/forms/schedule-form/schedule-form.js b/app/portainer/components/forms/schedule-form/schedule-form.js index 27b14790f..a483aef37 100644 --- a/app/portainer/components/forms/schedule-form/schedule-form.js +++ b/app/portainer/components/forms/schedule-form/schedule-form.js @@ -7,6 +7,38 @@ angular.module('portainer.app').component('scheduleForm', { formValidationError: '' }; + ctrl.scheduleValues = [{ + displayed: 'Every hour', + cron: '0 0 * * *' + }, + { + displayed: 'Every 2 hours', + cron: '0 0 0/2 * *' + }, { + displayed: 'Every day', + cron: '0 0 0 * *' + } + ]; + + ctrl.formValues = { + datetime: ctrl.model.CronExpression ? cronToDatetime(ctrl.model.CronExpression) : moment(), + scheduleValue: ctrl.scheduleValues[0], + cronMethod: 'basic' + }; + + function cronToDatetime(cron) { + strings = cron.split(' '); + if (strings.length !== 5) { + return moment(); + } + return moment(cron, 's m H D M'); + } + + function datetimeToCron(datetime) { + var date = moment(datetime); + return '0 '.concat(date.minutes(), ' ', date.hours(), ' ', date.date(), ' ', (date.month() + 1)); + } + this.action = function() { ctrl.state.formValidationError = ''; @@ -15,6 +47,15 @@ angular.module('portainer.app').component('scheduleForm', { return; } + if (ctrl.formValues.cronMethod === 'basic') { + if (ctrl.model.Recurring === false) { + ctrl.model.CronExpression = datetimeToCron(ctrl.formValues.datetime); + } else { + ctrl.model.CronExpression = ctrl.formValues.scheduleValue.cron; + } + } else { + ctrl.model.Recurring = true; + } ctrl.formAction(); }; diff --git a/app/portainer/components/forms/schedule-form/scheduleForm.html b/app/portainer/components/forms/schedule-form/scheduleForm.html index 4cdae3d96..0307b5dd5 100644 --- a/app/portainer/components/forms/schedule-form/scheduleForm.html +++ b/app/portainer/components/forms/schedule-form/scheduleForm.html @@ -18,24 +18,107 @@ -
This field is required.
+ +This field is required.
+This field is required.
+This field is required.
+