2018-11-05 20:58:15 +00:00
|
|
|
package schedules
|
|
|
|
|
|
|
|
import (
|
2019-07-25 22:38:07 +00:00
|
|
|
"encoding/base64"
|
2018-11-05 20:58:15 +00:00
|
|
|
"errors"
|
|
|
|
"net/http"
|
2018-11-07 04:19:10 +00:00
|
|
|
"strconv"
|
2019-07-25 22:38:07 +00:00
|
|
|
"strings"
|
2018-11-06 20:22:30 +00:00
|
|
|
"time"
|
2018-11-05 20:58:15 +00:00
|
|
|
|
|
|
|
"github.com/asaskevich/govalidator"
|
|
|
|
httperror "github.com/portainer/libhttp/error"
|
|
|
|
"github.com/portainer/libhttp/request"
|
|
|
|
"github.com/portainer/libhttp/response"
|
2019-03-21 01:20:14 +00:00
|
|
|
"github.com/portainer/portainer/api"
|
|
|
|
"github.com/portainer/portainer/api/cron"
|
2018-11-05 20:58:15 +00:00
|
|
|
)
|
|
|
|
|
2018-11-09 02:22:08 +00:00
|
|
|
type scheduleCreateFromFilePayload struct {
|
2018-11-05 20:58:15 +00:00
|
|
|
Name string
|
|
|
|
Image string
|
|
|
|
CronExpression string
|
2018-12-06 19:53:23 +00:00
|
|
|
Recurring bool
|
2018-11-05 20:58:15 +00:00
|
|
|
Endpoints []portainer.EndpointID
|
|
|
|
File []byte
|
2018-11-09 02:22:08 +00:00
|
|
|
RetryCount int
|
|
|
|
RetryInterval int
|
2018-11-05 20:58:15 +00:00
|
|
|
}
|
|
|
|
|
2018-11-09 02:22:08 +00:00
|
|
|
type scheduleCreateFromFileContentPayload struct {
|
2018-11-05 20:58:15 +00:00
|
|
|
Name string
|
|
|
|
CronExpression string
|
2018-12-06 19:53:23 +00:00
|
|
|
Recurring bool
|
2018-11-05 20:58:15 +00:00
|
|
|
Image string
|
|
|
|
Endpoints []portainer.EndpointID
|
|
|
|
FileContent string
|
2018-11-09 02:22:08 +00:00
|
|
|
RetryCount int
|
|
|
|
RetryInterval int
|
2018-11-05 20:58:15 +00:00
|
|
|
}
|
|
|
|
|
2018-11-09 02:22:08 +00:00
|
|
|
func (payload *scheduleCreateFromFilePayload) Validate(r *http.Request) error {
|
2018-11-05 20:58:15 +00:00
|
|
|
name, err := request.RetrieveMultiPartFormValue(r, "Name", false)
|
|
|
|
if err != nil {
|
2018-11-14 03:10:49 +00:00
|
|
|
return errors.New("Invalid schedule name")
|
|
|
|
}
|
|
|
|
|
|
|
|
if !govalidator.Matches(name, `^[a-zA-Z0-9][a-zA-Z0-9_.-]+$`) {
|
|
|
|
return errors.New("Invalid schedule name format. Allowed characters are: [a-zA-Z0-9_.-]")
|
2018-11-05 20:58:15 +00:00
|
|
|
}
|
|
|
|
payload.Name = name
|
|
|
|
|
|
|
|
image, err := request.RetrieveMultiPartFormValue(r, "Image", false)
|
|
|
|
if err != nil {
|
2018-11-14 03:10:49 +00:00
|
|
|
return errors.New("Invalid schedule image")
|
2018-11-05 20:58:15 +00:00
|
|
|
}
|
|
|
|
payload.Image = image
|
|
|
|
|
2018-11-06 22:59:21 +00:00
|
|
|
cronExpression, err := request.RetrieveMultiPartFormValue(r, "CronExpression", false)
|
2018-11-05 20:58:15 +00:00
|
|
|
if err != nil {
|
2018-11-06 22:59:21 +00:00
|
|
|
return errors.New("Invalid cron expression")
|
2018-11-05 20:58:15 +00:00
|
|
|
}
|
|
|
|
payload.CronExpression = cronExpression
|
|
|
|
|
|
|
|
var endpoints []portainer.EndpointID
|
|
|
|
err = request.RetrieveMultiPartFormJSONValue(r, "Endpoints", &endpoints, false)
|
|
|
|
if err != nil {
|
2018-11-06 22:59:21 +00:00
|
|
|
return errors.New("Invalid endpoints")
|
2018-11-05 20:58:15 +00:00
|
|
|
}
|
|
|
|
payload.Endpoints = endpoints
|
|
|
|
|
2018-11-06 22:59:21 +00:00
|
|
|
file, _, err := request.RetrieveMultiPartFormFile(r, "file")
|
2018-11-05 20:58:15 +00:00
|
|
|
if err != nil {
|
2018-11-06 22:59:21 +00:00
|
|
|
return portainer.Error("Invalid script file. Ensure that the file is uploaded correctly")
|
2018-11-05 20:58:15 +00:00
|
|
|
}
|
|
|
|
payload.File = file
|
|
|
|
|
2018-11-09 02:22:08 +00:00
|
|
|
retryCount, _ := request.RetrieveNumericMultiPartFormValue(r, "RetryCount", true)
|
|
|
|
payload.RetryCount = retryCount
|
|
|
|
|
|
|
|
retryInterval, _ := request.RetrieveNumericMultiPartFormValue(r, "RetryInterval", true)
|
|
|
|
payload.RetryInterval = retryInterval
|
|
|
|
|
2018-11-05 20:58:15 +00:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2018-11-09 02:22:08 +00:00
|
|
|
func (payload *scheduleCreateFromFileContentPayload) Validate(r *http.Request) error {
|
2018-11-05 20:58:15 +00:00
|
|
|
if govalidator.IsNull(payload.Name) {
|
|
|
|
return portainer.Error("Invalid schedule name")
|
|
|
|
}
|
|
|
|
|
2018-11-14 03:10:49 +00:00
|
|
|
if !govalidator.Matches(payload.Name, `^[a-zA-Z0-9][a-zA-Z0-9_.-]+$`) {
|
|
|
|
return errors.New("Invalid schedule name format. Allowed characters are: [a-zA-Z0-9_.-]")
|
|
|
|
}
|
|
|
|
|
2018-11-05 20:58:15 +00:00
|
|
|
if govalidator.IsNull(payload.Image) {
|
|
|
|
return portainer.Error("Invalid schedule image")
|
|
|
|
}
|
|
|
|
|
|
|
|
if govalidator.IsNull(payload.CronExpression) {
|
|
|
|
return portainer.Error("Invalid cron expression")
|
|
|
|
}
|
|
|
|
|
|
|
|
if payload.Endpoints == nil || len(payload.Endpoints) == 0 {
|
|
|
|
return portainer.Error("Invalid endpoints payload")
|
|
|
|
}
|
|
|
|
|
|
|
|
if govalidator.IsNull(payload.FileContent) {
|
|
|
|
return portainer.Error("Invalid script file content")
|
|
|
|
}
|
|
|
|
|
2018-11-09 02:22:08 +00:00
|
|
|
if payload.RetryCount != 0 && payload.RetryInterval == 0 {
|
|
|
|
return portainer.Error("RetryInterval must be set")
|
|
|
|
}
|
|
|
|
|
2018-11-05 20:58:15 +00:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2019-07-25 22:38:07 +00:00
|
|
|
// POST /api/schedules?method=file|string
|
2018-11-05 20:58:15 +00:00
|
|
|
func (handler *Handler) scheduleCreate(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
2018-12-05 22:36:25 +00:00
|
|
|
settings, err := handler.SettingsService.Settings()
|
|
|
|
if err != nil {
|
|
|
|
return &httperror.HandlerError{http.StatusServiceUnavailable, "Unable to retrieve settings", err}
|
|
|
|
}
|
|
|
|
if !settings.EnableHostManagementFeatures {
|
|
|
|
return &httperror.HandlerError{http.StatusServiceUnavailable, "Host management features are disabled", portainer.ErrHostManagementFeaturesDisabled}
|
|
|
|
}
|
|
|
|
|
2018-11-05 20:58:15 +00:00
|
|
|
method, err := request.RetrieveQueryParameter(r, "method", false)
|
|
|
|
if err != nil {
|
|
|
|
return &httperror.HandlerError{http.StatusBadRequest, "Invalid query parameter: method. Valid values are: file or string", err}
|
|
|
|
}
|
|
|
|
|
|
|
|
switch method {
|
|
|
|
case "string":
|
|
|
|
return handler.createScheduleFromFileContent(w, r)
|
|
|
|
case "file":
|
|
|
|
return handler.createScheduleFromFile(w, r)
|
|
|
|
default:
|
|
|
|
return &httperror.HandlerError{http.StatusBadRequest, "Invalid query parameter: method. Valid values are: file or string", errors.New(request.ErrInvalidQueryParameter)}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (handler *Handler) createScheduleFromFileContent(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
2018-11-09 02:22:08 +00:00
|
|
|
var payload scheduleCreateFromFileContentPayload
|
2018-11-05 20:58:15 +00:00
|
|
|
err := request.DecodeAndValidateJSONPayload(r, &payload)
|
|
|
|
if err != nil {
|
|
|
|
return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err}
|
|
|
|
}
|
|
|
|
|
2018-11-09 02:22:08 +00:00
|
|
|
schedule := handler.createScheduleObjectFromFileContentPayload(&payload)
|
|
|
|
|
|
|
|
err = handler.addAndPersistSchedule(schedule, []byte(payload.FileContent))
|
2018-11-05 20:58:15 +00:00
|
|
|
if err != nil {
|
2018-11-09 02:22:08 +00:00
|
|
|
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to schedule script job", err}
|
2018-11-05 20:58:15 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return response.JSON(w, schedule)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (handler *Handler) createScheduleFromFile(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
2018-11-09 02:22:08 +00:00
|
|
|
payload := &scheduleCreateFromFilePayload{}
|
2018-11-05 20:58:15 +00:00
|
|
|
err := payload.Validate(r)
|
|
|
|
if err != nil {
|
|
|
|
return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err}
|
|
|
|
}
|
|
|
|
|
2018-11-09 02:22:08 +00:00
|
|
|
schedule := handler.createScheduleObjectFromFilePayload(payload)
|
|
|
|
|
|
|
|
err = handler.addAndPersistSchedule(schedule, payload.File)
|
2018-11-05 20:58:15 +00:00
|
|
|
if err != nil {
|
2018-11-09 02:22:08 +00:00
|
|
|
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to schedule script job", err}
|
2018-11-05 20:58:15 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return response.JSON(w, schedule)
|
|
|
|
}
|
|
|
|
|
2018-11-09 02:22:08 +00:00
|
|
|
func (handler *Handler) createScheduleObjectFromFilePayload(payload *scheduleCreateFromFilePayload) *portainer.Schedule {
|
2018-11-05 20:58:15 +00:00
|
|
|
scheduleIdentifier := portainer.ScheduleID(handler.ScheduleService.GetNextIdentifier())
|
|
|
|
|
2018-11-09 02:22:08 +00:00
|
|
|
job := &portainer.ScriptExecutionJob{
|
2018-12-06 19:53:23 +00:00
|
|
|
Endpoints: payload.Endpoints,
|
|
|
|
Image: payload.Image,
|
2018-11-09 02:22:08 +00:00
|
|
|
RetryCount: payload.RetryCount,
|
|
|
|
RetryInterval: payload.RetryInterval,
|
|
|
|
}
|
|
|
|
|
|
|
|
schedule := &portainer.Schedule{
|
|
|
|
ID: scheduleIdentifier,
|
|
|
|
Name: payload.Name,
|
|
|
|
CronExpression: payload.CronExpression,
|
2018-12-06 19:53:23 +00:00
|
|
|
Recurring: payload.Recurring,
|
2018-11-09 02:22:08 +00:00
|
|
|
JobType: portainer.ScriptExecutionJobType,
|
|
|
|
ScriptExecutionJob: job,
|
|
|
|
Created: time.Now().Unix(),
|
2018-11-05 20:58:15 +00:00
|
|
|
}
|
|
|
|
|
2018-11-09 02:22:08 +00:00
|
|
|
return schedule
|
|
|
|
}
|
|
|
|
|
|
|
|
func (handler *Handler) createScheduleObjectFromFileContentPayload(payload *scheduleCreateFromFileContentPayload) *portainer.Schedule {
|
|
|
|
scheduleIdentifier := portainer.ScheduleID(handler.ScheduleService.GetNextIdentifier())
|
|
|
|
|
2018-11-06 09:49:48 +00:00
|
|
|
job := &portainer.ScriptExecutionJob{
|
2018-12-06 19:53:23 +00:00
|
|
|
Endpoints: payload.Endpoints,
|
|
|
|
Image: payload.Image,
|
2018-11-09 02:22:08 +00:00
|
|
|
RetryCount: payload.RetryCount,
|
|
|
|
RetryInterval: payload.RetryInterval,
|
2018-11-05 20:58:15 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
schedule := &portainer.Schedule{
|
2018-11-06 09:49:48 +00:00
|
|
|
ID: scheduleIdentifier,
|
2018-11-09 02:22:08 +00:00
|
|
|
Name: payload.Name,
|
|
|
|
CronExpression: payload.CronExpression,
|
2018-12-06 19:53:23 +00:00
|
|
|
Recurring: payload.Recurring,
|
2018-11-06 09:49:48 +00:00
|
|
|
JobType: portainer.ScriptExecutionJobType,
|
|
|
|
ScriptExecutionJob: job,
|
2018-11-06 20:22:30 +00:00
|
|
|
Created: time.Now().Unix(),
|
2018-11-06 09:49:48 +00:00
|
|
|
}
|
|
|
|
|
2018-11-09 02:22:08 +00:00
|
|
|
return schedule
|
|
|
|
}
|
2018-11-06 09:49:48 +00:00
|
|
|
|
2018-11-09 02:22:08 +00:00
|
|
|
func (handler *Handler) addAndPersistSchedule(schedule *portainer.Schedule, file []byte) error {
|
2019-07-25 22:38:07 +00:00
|
|
|
nonEdgeEndpointIDs := make([]portainer.EndpointID, 0)
|
|
|
|
edgeEndpointIDs := make([]portainer.EndpointID, 0)
|
|
|
|
|
|
|
|
edgeCronExpression := strings.Split(schedule.CronExpression, " ")
|
|
|
|
if len(edgeCronExpression) == 6 {
|
|
|
|
edgeCronExpression = edgeCronExpression[1:]
|
|
|
|
}
|
|
|
|
|
|
|
|
for _, ID := range schedule.ScriptExecutionJob.Endpoints {
|
|
|
|
|
|
|
|
endpoint, err := handler.EndpointService.Endpoint(ID)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
if endpoint.Type != portainer.EdgeAgentEnvironment {
|
|
|
|
nonEdgeEndpointIDs = append(nonEdgeEndpointIDs, endpoint.ID)
|
|
|
|
} else {
|
|
|
|
edgeEndpointIDs = append(edgeEndpointIDs, endpoint.ID)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if len(edgeEndpointIDs) > 0 {
|
|
|
|
edgeSchedule := &portainer.EdgeSchedule{
|
|
|
|
ID: schedule.ID,
|
|
|
|
CronExpression: strings.Join(edgeCronExpression, " "),
|
|
|
|
Script: base64.RawStdEncoding.EncodeToString(file),
|
|
|
|
Endpoints: edgeEndpointIDs,
|
|
|
|
Version: 1,
|
|
|
|
}
|
|
|
|
|
|
|
|
for _, endpointID := range edgeEndpointIDs {
|
|
|
|
handler.ReverseTunnelService.AddSchedule(endpointID, edgeSchedule)
|
|
|
|
}
|
|
|
|
|
|
|
|
schedule.EdgeSchedule = edgeSchedule
|
|
|
|
}
|
|
|
|
|
|
|
|
schedule.ScriptExecutionJob.Endpoints = nonEdgeEndpointIDs
|
|
|
|
|
2018-11-09 02:22:08 +00:00
|
|
|
scriptPath, err := handler.FileService.StoreScheduledJobFileFromBytes(strconv.Itoa(int(schedule.ID)), file)
|
2018-11-06 09:49:48 +00:00
|
|
|
if err != nil {
|
2018-11-09 02:22:08 +00:00
|
|
|
return err
|
2018-11-05 20:58:15 +00:00
|
|
|
}
|
|
|
|
|
2018-11-09 02:22:08 +00:00
|
|
|
schedule.ScriptExecutionJob.ScriptPath = scriptPath
|
|
|
|
|
|
|
|
jobContext := cron.NewScriptExecutionJobContext(handler.JobService, handler.EndpointService, handler.FileService)
|
2018-11-13 01:39:26 +00:00
|
|
|
jobRunner := cron.NewScriptExecutionJobRunner(schedule, jobContext)
|
2018-11-09 02:22:08 +00:00
|
|
|
|
2018-11-13 01:39:26 +00:00
|
|
|
err = handler.JobScheduler.ScheduleJob(jobRunner)
|
2018-11-05 20:58:15 +00:00
|
|
|
if err != nil {
|
2018-11-09 02:22:08 +00:00
|
|
|
return err
|
2018-11-05 20:58:15 +00:00
|
|
|
}
|
|
|
|
|
2018-11-09 02:22:08 +00:00
|
|
|
return handler.ScheduleService.CreateSchedule(schedule)
|
2018-11-05 20:58:15 +00:00
|
|
|
}
|