diff --git a/api/filesystem/filesystem.go b/api/filesystem/filesystem.go index a0c85727d..acbb12db5 100644 --- a/api/filesystem/filesystem.go +++ b/api/filesystem/filesystem.go @@ -5,7 +5,6 @@ import ( "encoding/json" "encoding/pem" "io/ioutil" - "strconv" "github.com/portainer/portainer" @@ -322,16 +321,15 @@ func (service *Service) getContentFromPEMFile(filePath string) ([]byte, error) { return block.Bytes, nil } -// GetScheduleFolder returns the absolute path on the FS for a schedule based +// GetScheduleFolder returns the absolute path on the filesystem for a schedule based // on its identifier. -func (service *Service) GetScheduleFolder(scheduleIdentifier portainer.ScheduleID) string { - return path.Join(service.fileStorePath, ScheduleStorePath, strconv.Itoa(int(scheduleIdentifier))) +func (service *Service) GetScheduleFolder(identifier string) string { + return path.Join(service.fileStorePath, ScheduleStorePath, identifier) } // StoreScheduledJobFileFromBytes creates a subfolder in the ScheduleStorePath and stores a new file from bytes. // It returns the path to the folder where the file is stored. -func (service *Service) StoreScheduledJobFileFromBytes(scheduleIdentifier portainer.ScheduleID, data []byte) (string, error) { - identifier := strconv.Itoa(int(scheduleIdentifier)) +func (service *Service) StoreScheduledJobFileFromBytes(identifier string, data []byte) (string, error) { scheduleStorePath := path.Join(ScheduleStorePath, identifier) err := service.createDirectoryInStore(scheduleStorePath) if err != nil { diff --git a/api/http/handler/schedules/handler.go b/api/http/handler/schedules/handler.go index 408c8c65c..073c05606 100644 --- a/api/http/handler/schedules/handler.go +++ b/api/http/handler/schedules/handler.go @@ -35,6 +35,7 @@ func NewHandler(bouncer *security.RequestBouncer) *Handler { bouncer.AdministratorAccess(httperror.LoggerHandler(h.scheduleUpdate))).Methods(http.MethodPut) h.Handle("/schedules/{id}", bouncer.AdministratorAccess(httperror.LoggerHandler(h.scheduleDelete))).Methods(http.MethodDelete) - + h.Handle("/schedules/{id}/file", + bouncer.AdministratorAccess(httperror.LoggerHandler(h.scheduleFile))).Methods(http.MethodGet) return h } diff --git a/api/http/handler/schedules/schedule_create.go b/api/http/handler/schedules/schedule_create.go index 4d8fbd740..625d707ca 100644 --- a/api/http/handler/schedules/schedule_create.go +++ b/api/http/handler/schedules/schedule_create.go @@ -3,6 +3,7 @@ package schedules import ( "errors" "net/http" + "strconv" "time" "github.com/asaskevich/govalidator" @@ -138,7 +139,7 @@ func (handler *Handler) createScheduleFromFile(w http.ResponseWriter, r *http.Re func (handler *Handler) createSchedule(name, image, cronExpression string, endpoints []portainer.EndpointID, file []byte) (*portainer.Schedule, error) { scheduleIdentifier := portainer.ScheduleID(handler.ScheduleService.GetNextIdentifier()) - scriptPath, err := handler.FileService.StoreScheduledJobFileFromBytes(scheduleIdentifier, file) + scriptPath, err := handler.FileService.StoreScheduledJobFileFromBytes(strconv.Itoa(int(scheduleIdentifier)), file) if err != nil { return nil, err } diff --git a/api/http/handler/schedules/schedule_delete.go b/api/http/handler/schedules/schedule_delete.go index 502f7d7e4..51c01eec9 100644 --- a/api/http/handler/schedules/schedule_delete.go +++ b/api/http/handler/schedules/schedule_delete.go @@ -3,6 +3,7 @@ package schedules import ( "errors" "net/http" + "strconv" httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/request" @@ -27,7 +28,7 @@ func (handler *Handler) scheduleDelete(w http.ResponseWriter, r *http.Request) * return &httperror.HandlerError{http.StatusBadRequest, "Cannot remove system schedules", errors.New("Cannot remove system schedule")} } - scheduleFolder := handler.FileService.GetScheduleFolder(portainer.ScheduleID(scheduleID)) + scheduleFolder := handler.FileService.GetScheduleFolder(strconv.Itoa(scheduleID)) err = handler.FileService.RemoveDirectory(scheduleFolder) if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to remove the files associated to the schedule on the filesystem", err} diff --git a/api/http/handler/schedules/schedule_file.go b/api/http/handler/schedules/schedule_file.go new file mode 100644 index 000000000..790f4d2e4 --- /dev/null +++ b/api/http/handler/schedules/schedule_file.go @@ -0,0 +1,41 @@ +package schedules + +import ( + "errors" + "net/http" + + httperror "github.com/portainer/libhttp/error" + "github.com/portainer/libhttp/request" + "github.com/portainer/libhttp/response" + "github.com/portainer/portainer" +) + +type scheduleFileResponse struct { + ScheduleFileContent string `json:"ScheduleFileContent"` +} + +// GET request on /api/schedules/:id/file +func (handler *Handler) scheduleFile(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { + scheduleID, err := request.RetrieveNumericRouteVariableValue(r, "id") + if err != nil { + return &httperror.HandlerError{http.StatusBadRequest, "Invalid schedule identifier route variable", err} + } + + schedule, err := handler.ScheduleService.Schedule(portainer.ScheduleID(scheduleID)) + if err == portainer.ErrObjectNotFound { + return &httperror.HandlerError{http.StatusNotFound, "Unable to find a schedule with the specified identifier inside the database", err} + } else if err != nil { + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a schedule with the specified identifier inside the database", err} + } + + if schedule.JobType != portainer.ScriptExecutionJobType { + return &httperror.HandlerError{http.StatusBadRequest, "Unable to retrieve script file", errors.New("This type of schedule do not have any associated script file")} + } + + scheduleFileContent, err := handler.FileService.GetFileContent(schedule.ScriptExecutionJob.ScriptPath) + if err != nil { + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve schedule script file from disk", err} + } + + return response.JSON(w, &scheduleFileResponse{ScheduleFileContent: string(scheduleFileContent)}) +} diff --git a/api/http/handler/schedules/schedule_update.go b/api/http/handler/schedules/schedule_update.go index 209c47da0..e4de5b5f1 100644 --- a/api/http/handler/schedules/schedule_update.go +++ b/api/http/handler/schedules/schedule_update.go @@ -2,6 +2,7 @@ package schedules import ( "net/http" + "strconv" httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/request" @@ -15,6 +16,7 @@ type scheduleUpdatePayload struct { Image *string CronExpression *string Endpoints []portainer.EndpointID + FileContent *string } func (payload *scheduleUpdatePayload) Validate(r *http.Request) error { @@ -41,8 +43,16 @@ func (handler *Handler) scheduleUpdate(w http.ResponseWriter, r *http.Request) * } updateJobSchedule := updateSchedule(schedule, &payload) - if updateJobSchedule { + if payload.FileContent != nil { + _, err := handler.FileService.StoreScheduledJobFileFromBytes(strconv.Itoa(scheduleID), []byte(*payload.FileContent)) + if err != nil { + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist script file changes on the filesystem", err} + } + updateJobSchedule = true + } + + if updateJobSchedule { jobContext := cron.NewScriptExecutionJobContext(handler.JobService, handler.EndpointService, handler.FileService) jobRunner := cron.NewScriptExecutionJobRunner(schedule.ScriptExecutionJob, jobContext) err := handler.JobScheduler.UpdateSchedule(schedule, jobRunner) diff --git a/api/portainer.go b/api/portainer.go index b337c6d4b..9e3efb8e8 100644 --- a/api/portainer.go +++ b/api/portainer.go @@ -654,8 +654,8 @@ type ( LoadKeyPair() ([]byte, []byte, error) WriteJSONToFile(path string, content interface{}) error FileExists(path string) (bool, error) - StoreScheduledJobFileFromBytes(scheduleIdentifier ScheduleID, data []byte) (string, error) - GetScheduleFolder(scheduleIdentifier ScheduleID) string + StoreScheduledJobFileFromBytes(identifier string, data []byte) (string, error) + GetScheduleFolder(identifier string) string } // GitService represents a service for managing Git diff --git a/app/portainer/components/forms/schedule-form/scheduleForm.html b/app/portainer/components/forms/schedule-form/scheduleForm.html index 1f287ece1..02ed2ebbe 100644 --- a/app/portainer/components/forms/schedule-form/scheduleForm.html +++ b/app/portainer/components/forms/schedule-form/scheduleForm.html @@ -55,14 +55,14 @@ +
+ + This schedule will be executed via a privileged container on the target hosts. You can access the host filesystem under the + /host folder. + +
+
-
- - This schedule will be executed via a privileged container on the target hosts. You can access the host filesystem under the - /host folder. - -
-
Job content
@@ -91,46 +91,46 @@
- - -
-
- Web editor -
-
-
- -
+
+ + +
+
+ Web editor +
+
+
+
- - -
-
- Upload -
-
- - You can upload a script file from your computer. +
+ + +
+
+ Upload +
+
+ + You can upload a script file from your computer. + +
+
+
+ + + {{ $ctrl.model.Job.File.name }} +
-
-
- - - {{ $ctrl.model.Job.File.name }} - - -
-
-
+
Target endpoints
diff --git a/app/portainer/models/schedule.js b/app/portainer/models/schedule.js index a3bb7cd45..7dcdde119 100644 --- a/app/portainer/models/schedule.js +++ b/app/portainer/models/schedule.js @@ -27,6 +27,8 @@ function ScheduleModel(data) { function ScriptExecutionJobModel(data) { this.Image = data.Image; this.Endpoints = data.Endpoints; + this.FileContent = ''; + this.Method = 'editor'; } function ScheduleCreateRequest(model) { @@ -44,4 +46,5 @@ function ScheduleUpdateRequest(model) { this.CronExpression = model.CronExpression; this.Image = model.Job.Image; this.Endpoints = model.Job.Endpoints; + this.FileContent = model.Job.FileContent; } diff --git a/app/portainer/rest/schedule.js b/app/portainer/rest/schedule.js index 8bd2fa624..df57ef68e 100644 --- a/app/portainer/rest/schedule.js +++ b/app/portainer/rest/schedule.js @@ -2,11 +2,12 @@ angular.module('portainer.app') .factory('Schedules', ['$resource', 'API_ENDPOINT_SCHEDULES', function SchedulesFactory($resource, API_ENDPOINT_SCHEDULES) { 'use strict'; - return $resource(API_ENDPOINT_SCHEDULES + '/:id', {}, { + return $resource(API_ENDPOINT_SCHEDULES + '/:id/:action', {}, { create: { method: 'POST' }, query: { method: 'GET', isArray: true }, get: { method: 'GET', params: { id: '@id' } }, update: { method: 'PUT', params: { id: '@id' } }, - remove: { method: 'DELETE', params: { id: '@id'} } + remove: { method: 'DELETE', params: { id: '@id'} }, + file: { method: 'GET', params: { id : '@id', action: 'file' } } }); }]); diff --git a/app/portainer/services/api/scheduleService.js b/app/portainer/services/api/scheduleService.js index f7723ef09..e1698b9c1 100644 --- a/app/portainer/services/api/scheduleService.js +++ b/app/portainer/services/api/scheduleService.js @@ -55,5 +55,9 @@ function ScheduleService($q, Schedules, FileUploadService) { return Schedules.remove({ id: scheduleId }).$promise; }; + service.getScriptFile = function(scheduleId) { + return Schedules.file({ id: scheduleId }).$promise; + }; + return service; }]); diff --git a/app/portainer/views/schedules/edit/schedule.html b/app/portainer/views/schedules/edit/schedule.html index f049459d6..654ac8d50 100644 --- a/app/portainer/views/schedules/edit/schedule.html +++ b/app/portainer/views/schedules/edit/schedule.html @@ -14,6 +14,7 @@