mirror of
https://github.com/portainer/portainer.git
synced 2025-07-24 15:59:41 +02:00
feat(edge-compute): move host jobs to edge (#3840)
* feat(endpoints): create an associated endpoints selector * feat(schedules): remove edge specific explanations * refactor(schedule): replace multi-endpoint-selector * refactor(schedule): move controller to single file * refactor(endpoints): remove multi-endpoint-selector * feat(edge): rename host jobs to edge jobs * feat(edge-jobs): remove edge warning * refactor(edge-jobs): move schedule pages to edge * refactor(edge-jobs): mv views to edgeJobs * refactor(edge-jobs): rename edge jobs * refactor(edge-jobs): move services to edge * refactor(edge-jobs): move tasks datatable * fix(edge-jobs): fix import * fix(edge-jobs): use right services * feat(settings): adjust host management description * feat(edge-jobs): introduce interfaces and types * feat(edge-jobs): implement bolt service * refactor(edge-jobs): replace schedule routes * refactor(edge-job): replace Schedule service * refactor(edge-jobs): remove job_script_exec * refactor(host): remove jobs table * feat(edge-jobs): replace schedule * feat(edge-jobs): load file on inspect * fix(edge-job): parse cron correctly * feat(edge-jobs): show tasks * feat(host): rename tooltip * refactor(host): remove old components * refactor(main): remove schedule types * refactor(snapshot): replace job service with snapshot service * refactor(jobs): remove jobs form and datatable * feat(edge-jobs): create db migration * fix(main): start snapshot service with correct interval * feat(settings): change host tooltip * feat(edge-jobs): load endpoints * fix(edge-job): disable form submit when form is invalid * refactor(edge-compute): use const * refactor(edge-jobs): use generic controller * refactor(edge-jobs): replace $scope with controllerAs * refactor(edge-jobs): replace routes with components * refactor(edge-jobs): replace functions with classes * refactor(edge-jobs): use async/await * refactor(edge-jobs): rename functions * feat(edge-jobs): introduce beta panel * feat(edge-jobs): allow single character names * fix(snapshot): run snapshot in coroutine * feat(edge-jobs): add logs status * feat(filesystem): add edge job logs methods * feat(edge-jobs): intoduce edge jobs tasks api * feat(edge-jobs): remove schedule task model * fix(fs): build edge job task file path * fix(edge-jobs): update task meta * fix(edge-jobs): return a list of endpoints * feat(edge-jobs): update logs from agent * feat(edge-jobs): collect logs * feat(edge-jobs): rename url * feat(edge-jobs): refresh to same tab * feat(edge-jobs): remove old info * refactor(edge-jobs): rename script path json * fix(edge-job): save file before adding job * feat(edge-job): show retrieving logs label * feat(edge-job): replace cron with 5 places * refactor(edge-jobs): replace tasks with results * feat(edge-jobs): add auto refresh until logs are collected * feat(edge-jobs): fix column size * feat(edge-job): display editor * feat(edge-job): add name validation * feat(edge-job): set default time for 1 hour from now * feat(edge-job): add validation for cron format * feat(edge-job): add a note about timezone * fix(edge-job): replace regex * fix(edge-job): check for every minute cron * style(edge-jobs): add reference for cron regex * refactor(edge-jobs): rename migration name * refactor(edge-job): rename edge job response * refactor(snapshot): rename snapshot endpoint method * refactor(edge-jobs): move tasks handler to edgejobs * feat(security): introduce a middleware for edge compute operations * feat(edge-job): use edge compute middleware * feat(edge-groups): filter http actions based on edge setting * fix(security): return from edge bouncer if failed * feat(edge-stacks): filter http actions based on edge setting * feat(edge-groups): show error when failed to load groups * refactor(db): remove edge-jobs migration * refactor(migrator): remove unused dependency Co-authored-by: Anthony Lapenna <lapenna.anthony@gmail.com>
This commit is contained in:
parent
b6f5d8f90e
commit
24528ecea8
120 changed files with 2624 additions and 3484 deletions
|
@ -21,14 +21,14 @@ func NewHandler(bouncer *security.RequestBouncer) *Handler {
|
|||
Router: mux.NewRouter(),
|
||||
}
|
||||
h.Handle("/edge_groups",
|
||||
bouncer.AdminAccess(httperror.LoggerHandler(h.edgeGroupCreate))).Methods(http.MethodPost)
|
||||
bouncer.AdminAccess(bouncer.EdgeComputeOperation(httperror.LoggerHandler(h.edgeGroupCreate)))).Methods(http.MethodPost)
|
||||
h.Handle("/edge_groups",
|
||||
bouncer.AdminAccess(httperror.LoggerHandler(h.edgeGroupList))).Methods(http.MethodGet)
|
||||
bouncer.AdminAccess(bouncer.EdgeComputeOperation(httperror.LoggerHandler(h.edgeGroupList)))).Methods(http.MethodGet)
|
||||
h.Handle("/edge_groups/{id}",
|
||||
bouncer.AdminAccess(httperror.LoggerHandler(h.edgeGroupInspect))).Methods(http.MethodGet)
|
||||
bouncer.AdminAccess(bouncer.EdgeComputeOperation(httperror.LoggerHandler(h.edgeGroupInspect)))).Methods(http.MethodGet)
|
||||
h.Handle("/edge_groups/{id}",
|
||||
bouncer.AdminAccess(httperror.LoggerHandler(h.edgeGroupUpdate))).Methods(http.MethodPut)
|
||||
bouncer.AdminAccess(bouncer.EdgeComputeOperation(httperror.LoggerHandler(h.edgeGroupUpdate)))).Methods(http.MethodPut)
|
||||
h.Handle("/edge_groups/{id}",
|
||||
bouncer.AdminAccess(httperror.LoggerHandler(h.edgeGroupDelete))).Methods(http.MethodDelete)
|
||||
bouncer.AdminAccess(bouncer.EdgeComputeOperation(httperror.LoggerHandler(h.edgeGroupDelete)))).Methods(http.MethodDelete)
|
||||
return h
|
||||
}
|
||||
|
|
220
api/http/handler/edgejobs/edgejob_create.go
Normal file
220
api/http/handler/edgejobs/edgejob_create.go
Normal file
|
@ -0,0 +1,220 @@
|
|||
package edgejobs
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/asaskevich/govalidator"
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
"github.com/portainer/libhttp/response"
|
||||
"github.com/portainer/portainer/api"
|
||||
)
|
||||
|
||||
// POST /api/edge_jobs?method=file|string
|
||||
func (handler *Handler) edgeJobCreate(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
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.createEdgeJobFromFileContent(w, r)
|
||||
case "file":
|
||||
return handler.createEdgeJobFromFile(w, r)
|
||||
default:
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid query parameter: method. Valid values are: file or string", errors.New(request.ErrInvalidQueryParameter)}
|
||||
}
|
||||
}
|
||||
|
||||
type edgeJobCreateFromFileContentPayload struct {
|
||||
Name string
|
||||
CronExpression string
|
||||
Recurring bool
|
||||
Endpoints []portainer.EndpointID
|
||||
FileContent string
|
||||
}
|
||||
|
||||
func (payload *edgeJobCreateFromFileContentPayload) Validate(r *http.Request) error {
|
||||
if govalidator.IsNull(payload.Name) {
|
||||
return portainer.Error("Invalid Edge job name")
|
||||
}
|
||||
|
||||
if !govalidator.Matches(payload.Name, `^[a-zA-Z0-9][a-zA-Z0-9_.-]*$`) {
|
||||
return errors.New("Invalid Edge job name format. Allowed characters are: [a-zA-Z0-9_.-]")
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (handler *Handler) createEdgeJobFromFileContent(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
var payload edgeJobCreateFromFileContentPayload
|
||||
err := request.DecodeAndValidateJSONPayload(r, &payload)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err}
|
||||
}
|
||||
|
||||
edgeJob := handler.createEdgeJobObjectFromFileContentPayload(&payload)
|
||||
|
||||
err = handler.addAndPersistEdgeJob(edgeJob, []byte(payload.FileContent))
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to schedule Edge job", err}
|
||||
}
|
||||
|
||||
return response.JSON(w, edgeJob)
|
||||
}
|
||||
|
||||
type edgeJobCreateFromFilePayload struct {
|
||||
Name string
|
||||
CronExpression string
|
||||
Recurring bool
|
||||
Endpoints []portainer.EndpointID
|
||||
File []byte
|
||||
}
|
||||
|
||||
func (payload *edgeJobCreateFromFilePayload) Validate(r *http.Request) error {
|
||||
name, err := request.RetrieveMultiPartFormValue(r, "Name", false)
|
||||
if err != nil {
|
||||
return errors.New("Invalid Edge job name")
|
||||
}
|
||||
|
||||
if !govalidator.Matches(name, `^[a-zA-Z0-9][a-zA-Z0-9_.-]+$`) {
|
||||
return errors.New("Invalid Edge job name format. Allowed characters are: [a-zA-Z0-9_.-]")
|
||||
}
|
||||
payload.Name = name
|
||||
|
||||
cronExpression, err := request.RetrieveMultiPartFormValue(r, "CronExpression", false)
|
||||
if err != nil {
|
||||
return errors.New("Invalid cron expression")
|
||||
}
|
||||
payload.CronExpression = cronExpression
|
||||
|
||||
var endpoints []portainer.EndpointID
|
||||
err = request.RetrieveMultiPartFormJSONValue(r, "Endpoints", &endpoints, false)
|
||||
if err != nil {
|
||||
return errors.New("Invalid endpoints")
|
||||
}
|
||||
payload.Endpoints = endpoints
|
||||
|
||||
file, _, err := request.RetrieveMultiPartFormFile(r, "file")
|
||||
if err != nil {
|
||||
return portainer.Error("Invalid script file. Ensure that the file is uploaded correctly")
|
||||
}
|
||||
payload.File = file
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (handler *Handler) createEdgeJobFromFile(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
payload := &edgeJobCreateFromFilePayload{}
|
||||
err := payload.Validate(r)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err}
|
||||
}
|
||||
|
||||
edgeJob := handler.createEdgeJobObjectFromFilePayload(payload)
|
||||
|
||||
err = handler.addAndPersistEdgeJob(edgeJob, payload.File)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to schedule Edge job", err}
|
||||
}
|
||||
|
||||
return response.JSON(w, edgeJob)
|
||||
}
|
||||
|
||||
func (handler *Handler) createEdgeJobObjectFromFilePayload(payload *edgeJobCreateFromFilePayload) *portainer.EdgeJob {
|
||||
edgeJobIdentifier := portainer.EdgeJobID(handler.DataStore.EdgeJob().GetNextIdentifier())
|
||||
|
||||
endpoints := convertEndpointsToMetaObject(payload.Endpoints)
|
||||
|
||||
edgeJob := &portainer.EdgeJob{
|
||||
ID: edgeJobIdentifier,
|
||||
Name: payload.Name,
|
||||
CronExpression: payload.CronExpression,
|
||||
Recurring: payload.Recurring,
|
||||
Created: time.Now().Unix(),
|
||||
Endpoints: endpoints,
|
||||
Version: 1,
|
||||
}
|
||||
|
||||
return edgeJob
|
||||
}
|
||||
|
||||
func (handler *Handler) createEdgeJobObjectFromFileContentPayload(payload *edgeJobCreateFromFileContentPayload) *portainer.EdgeJob {
|
||||
edgeJobIdentifier := portainer.EdgeJobID(handler.DataStore.EdgeJob().GetNextIdentifier())
|
||||
|
||||
endpoints := convertEndpointsToMetaObject(payload.Endpoints)
|
||||
|
||||
edgeJob := &portainer.EdgeJob{
|
||||
ID: edgeJobIdentifier,
|
||||
Name: payload.Name,
|
||||
CronExpression: payload.CronExpression,
|
||||
Recurring: payload.Recurring,
|
||||
Created: time.Now().Unix(),
|
||||
Endpoints: endpoints,
|
||||
Version: 1,
|
||||
}
|
||||
|
||||
return edgeJob
|
||||
}
|
||||
|
||||
func (handler *Handler) addAndPersistEdgeJob(edgeJob *portainer.EdgeJob, file []byte) error {
|
||||
edgeCronExpression := strings.Split(edgeJob.CronExpression, " ")
|
||||
if len(edgeCronExpression) == 6 {
|
||||
edgeCronExpression = edgeCronExpression[1:]
|
||||
}
|
||||
edgeJob.CronExpression = strings.Join(edgeCronExpression, " ")
|
||||
|
||||
for ID := range edgeJob.Endpoints {
|
||||
endpoint, err := handler.DataStore.Endpoint().Endpoint(ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if endpoint.Type != portainer.EdgeAgentEnvironment {
|
||||
delete(edgeJob.Endpoints, ID)
|
||||
}
|
||||
}
|
||||
|
||||
if len(edgeJob.Endpoints) == 0 {
|
||||
return errors.New("Endpoints are mandatory for an Edge job")
|
||||
}
|
||||
|
||||
scriptPath, err := handler.FileService.StoreEdgeJobFileFromBytes(strconv.Itoa(int(edgeJob.ID)), file)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
edgeJob.ScriptPath = scriptPath
|
||||
|
||||
for endpointID := range edgeJob.Endpoints {
|
||||
handler.ReverseTunnelService.AddEdgeJob(endpointID, edgeJob)
|
||||
}
|
||||
|
||||
return handler.DataStore.EdgeJob().CreateEdgeJob(edgeJob)
|
||||
}
|
||||
|
||||
func convertEndpointsToMetaObject(endpoints []portainer.EndpointID) map[portainer.EndpointID]portainer.EdgeJobEndpointMeta {
|
||||
endpointsMap := map[portainer.EndpointID]portainer.EdgeJobEndpointMeta{}
|
||||
|
||||
for _, endpointID := range endpoints {
|
||||
endpointsMap[endpointID] = portainer.EdgeJobEndpointMeta{}
|
||||
}
|
||||
|
||||
return endpointsMap
|
||||
}
|
40
api/http/handler/edgejobs/edgejob_delete.go
Normal file
40
api/http/handler/edgejobs/edgejob_delete.go
Normal file
|
@ -0,0 +1,40 @@
|
|||
package edgejobs
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
"github.com/portainer/libhttp/response"
|
||||
"github.com/portainer/portainer/api"
|
||||
)
|
||||
|
||||
func (handler *Handler) edgeJobDelete(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
edgeJobID, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid Edge job identifier route variable", err}
|
||||
}
|
||||
|
||||
edgeJob, err := handler.DataStore.EdgeJob().EdgeJob(portainer.EdgeJobID(edgeJobID))
|
||||
if err == portainer.ErrObjectNotFound {
|
||||
return &httperror.HandlerError{http.StatusNotFound, "Unable to find an Edge job with the specified identifier inside the database", err}
|
||||
} else if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an Edge job with the specified identifier inside the database", err}
|
||||
}
|
||||
|
||||
edgeJobFolder := handler.FileService.GetEdgeJobFolder(strconv.Itoa(edgeJobID))
|
||||
err = handler.FileService.RemoveDirectory(edgeJobFolder)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to remove the files associated to the Edge job on the filesystem", err}
|
||||
}
|
||||
|
||||
handler.ReverseTunnelService.RemoveEdgeJob(edgeJob.ID)
|
||||
|
||||
err = handler.DataStore.EdgeJob().DeleteEdgeJob(edgeJob.ID)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to remove the Edge job from the database", err}
|
||||
}
|
||||
|
||||
return response.Empty(w)
|
||||
}
|
36
api/http/handler/edgejobs/edgejob_file.go
Normal file
36
api/http/handler/edgejobs/edgejob_file.go
Normal file
|
@ -0,0 +1,36 @@
|
|||
package edgejobs
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
"github.com/portainer/libhttp/response"
|
||||
"github.com/portainer/portainer/api"
|
||||
)
|
||||
|
||||
type edgeJobFileResponse struct {
|
||||
FileContent string `json:"FileContent"`
|
||||
}
|
||||
|
||||
// GET request on /api/edge_jobs/:id/file
|
||||
func (handler *Handler) edgeJobFile(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
edgeJobID, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid Edge job identifier route variable", err}
|
||||
}
|
||||
|
||||
edgeJob, err := handler.DataStore.EdgeJob().EdgeJob(portainer.EdgeJobID(edgeJobID))
|
||||
if err == portainer.ErrObjectNotFound {
|
||||
return &httperror.HandlerError{http.StatusNotFound, "Unable to find an Edge job with the specified identifier inside the database", err}
|
||||
} else if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an Edge job with the specified identifier inside the database", err}
|
||||
}
|
||||
|
||||
edgeJobFileContent, err := handler.FileService.GetFileContent(edgeJob.ScriptPath)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve Edge job script file from disk", err}
|
||||
}
|
||||
|
||||
return response.JSON(w, &edgeJobFileResponse{FileContent: string(edgeJobFileContent)})
|
||||
}
|
43
api/http/handler/edgejobs/edgejob_inspect.go
Normal file
43
api/http/handler/edgejobs/edgejob_inspect.go
Normal file
|
@ -0,0 +1,43 @@
|
|||
package edgejobs
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/portainer/libhttp/response"
|
||||
"github.com/portainer/portainer/api"
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
)
|
||||
|
||||
type edgeJobInspectResponse struct {
|
||||
*portainer.EdgeJob
|
||||
Endpoints []portainer.EndpointID
|
||||
}
|
||||
|
||||
func (handler *Handler) edgeJobInspect(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
edgeJobID, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid Edge job identifier route variable", err}
|
||||
}
|
||||
|
||||
edgeJob, err := handler.DataStore.EdgeJob().EdgeJob(portainer.EdgeJobID(edgeJobID))
|
||||
if err == portainer.ErrObjectNotFound {
|
||||
return &httperror.HandlerError{http.StatusNotFound, "Unable to find an Edge job with the specified identifier inside the database", err}
|
||||
} else if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an Edge job with the specified identifier inside the database", err}
|
||||
}
|
||||
|
||||
endpointIDs := []portainer.EndpointID{}
|
||||
|
||||
for endpointID := range edgeJob.Endpoints {
|
||||
endpointIDs = append(endpointIDs, endpointID)
|
||||
}
|
||||
|
||||
responseObj := edgeJobInspectResponse{
|
||||
EdgeJob: edgeJob,
|
||||
Endpoints: endpointIDs,
|
||||
}
|
||||
|
||||
return response.JSON(w, responseObj)
|
||||
}
|
18
api/http/handler/edgejobs/edgejob_list.go
Normal file
18
api/http/handler/edgejobs/edgejob_list.go
Normal file
|
@ -0,0 +1,18 @@
|
|||
package edgejobs
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/response"
|
||||
)
|
||||
|
||||
// GET request on /api/edge_jobs
|
||||
func (handler *Handler) edgeJobList(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
edgeJobs, err := handler.DataStore.EdgeJob().EdgeJobs()
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve Edge jobs from the database", err}
|
||||
}
|
||||
|
||||
return response.JSON(w, edgeJobs)
|
||||
}
|
52
api/http/handler/edgejobs/edgejob_tasklogs_clear.go
Normal file
52
api/http/handler/edgejobs/edgejob_tasklogs_clear.go
Normal file
|
@ -0,0 +1,52 @@
|
|||
package edgejobs
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
"github.com/portainer/libhttp/response"
|
||||
"github.com/portainer/portainer/api"
|
||||
)
|
||||
|
||||
// DELETE request on /api/edge_jobs/:id/tasks/:taskID/logs
|
||||
func (handler *Handler) edgeJobTasksClear(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
edgeJobID, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid Edge job identifier route variable", err}
|
||||
}
|
||||
|
||||
taskID, err := request.RetrieveNumericRouteVariableValue(r, "taskID")
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid Task identifier route variable", err}
|
||||
}
|
||||
|
||||
edgeJob, err := handler.DataStore.EdgeJob().EdgeJob(portainer.EdgeJobID(edgeJobID))
|
||||
if err == portainer.ErrObjectNotFound {
|
||||
return &httperror.HandlerError{http.StatusNotFound, "Unable to find an Edge job with the specified identifier inside the database", err}
|
||||
} else if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an Edge job with the specified identifier inside the database", err}
|
||||
}
|
||||
|
||||
endpointID := portainer.EndpointID(taskID)
|
||||
|
||||
meta := edgeJob.Endpoints[endpointID]
|
||||
meta.CollectLogs = false
|
||||
meta.LogsStatus = portainer.EdgeJobLogsStatusIdle
|
||||
edgeJob.Endpoints[endpointID] = meta
|
||||
|
||||
err = handler.FileService.ClearEdgeJobTaskLogs(strconv.Itoa(edgeJobID), strconv.Itoa(taskID))
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to clear log file from disk", err}
|
||||
}
|
||||
|
||||
handler.ReverseTunnelService.AddEdgeJob(endpointID, edgeJob)
|
||||
|
||||
err = handler.DataStore.EdgeJob().UpdateEdgeJob(edgeJob.ID, edgeJob)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist Edge job changes in the database", err}
|
||||
}
|
||||
|
||||
return response.Empty(w)
|
||||
}
|
46
api/http/handler/edgejobs/edgejob_tasklogs_collect.go
Normal file
46
api/http/handler/edgejobs/edgejob_tasklogs_collect.go
Normal file
|
@ -0,0 +1,46 @@
|
|||
package edgejobs
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
"github.com/portainer/libhttp/response"
|
||||
"github.com/portainer/portainer/api"
|
||||
)
|
||||
|
||||
// POST request on /api/edge_jobs/:id/tasks/:taskID/logs
|
||||
func (handler *Handler) edgeJobTasksCollect(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
edgeJobID, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid Edge job identifier route variable", err}
|
||||
}
|
||||
|
||||
taskID, err := request.RetrieveNumericRouteVariableValue(r, "taskID")
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid Task identifier route variable", err}
|
||||
}
|
||||
|
||||
edgeJob, err := handler.DataStore.EdgeJob().EdgeJob(portainer.EdgeJobID(edgeJobID))
|
||||
if err == portainer.ErrObjectNotFound {
|
||||
return &httperror.HandlerError{http.StatusNotFound, "Unable to find an Edge job with the specified identifier inside the database", err}
|
||||
} else if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an Edge job with the specified identifier inside the database", err}
|
||||
}
|
||||
|
||||
endpointID := portainer.EndpointID(taskID)
|
||||
|
||||
meta := edgeJob.Endpoints[endpointID]
|
||||
meta.CollectLogs = true
|
||||
meta.LogsStatus = portainer.EdgeJobLogsStatusPending
|
||||
edgeJob.Endpoints[endpointID] = meta
|
||||
|
||||
handler.ReverseTunnelService.AddEdgeJob(endpointID, edgeJob)
|
||||
|
||||
err = handler.DataStore.EdgeJob().UpdateEdgeJob(edgeJob.ID, edgeJob)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist Edge job changes in the database", err}
|
||||
}
|
||||
|
||||
return response.Empty(w)
|
||||
}
|
36
api/http/handler/edgejobs/edgejob_tasklogs_inspect.go
Normal file
36
api/http/handler/edgejobs/edgejob_tasklogs_inspect.go
Normal file
|
@ -0,0 +1,36 @@
|
|||
package edgejobs
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
"github.com/portainer/libhttp/response"
|
||||
)
|
||||
|
||||
type fileResponse struct {
|
||||
FileContent string `json:"FileContent"`
|
||||
}
|
||||
|
||||
// GET request on /api/edge_jobs/:id/tasks/:taskID/logs
|
||||
func (handler *Handler) edgeJobTaskLogsInspect(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
edgeJobID, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid Edge job identifier route variable", err}
|
||||
}
|
||||
|
||||
taskID, err := request.RetrieveNumericRouteVariableValue(r, "taskID")
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid Task identifier route variable", err}
|
||||
}
|
||||
|
||||
logFileContent, err := handler.FileService.GetEdgeJobTaskLogFileContent(strconv.Itoa(edgeJobID), strconv.Itoa(taskID))
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve log file from disk", err}
|
||||
}
|
||||
|
||||
return response.JSON(w, &fileResponse{FileContent: string(logFileContent)})
|
||||
}
|
||||
|
||||
// fmt.Sprintf("/tmp/edge_jobs/%s/logs_%s", edgeJobID, taskID)
|
56
api/http/handler/edgejobs/edgejob_tasks_list.go
Normal file
56
api/http/handler/edgejobs/edgejob_tasks_list.go
Normal file
|
@ -0,0 +1,56 @@
|
|||
package edgejobs
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
"github.com/portainer/libhttp/response"
|
||||
"github.com/portainer/portainer/api"
|
||||
)
|
||||
|
||||
type taskContainer struct {
|
||||
ID string `json:"Id"`
|
||||
EndpointID portainer.EndpointID `json:"EndpointId"`
|
||||
LogsStatus portainer.EdgeJobLogsStatus `json:"LogsStatus"`
|
||||
}
|
||||
|
||||
// GET request on /api/edge_jobs/:id/tasks
|
||||
func (handler *Handler) edgeJobTasksList(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
settings, err := handler.DataStore.Settings().Settings()
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusServiceUnavailable, "Unable to retrieve settings", err}
|
||||
}
|
||||
|
||||
if !settings.EnableEdgeComputeFeatures {
|
||||
return &httperror.HandlerError{http.StatusServiceUnavailable, "Edge compute features are disabled", portainer.ErrHostManagementFeaturesDisabled}
|
||||
}
|
||||
|
||||
edgeJobID, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid Edge job identifier route variable", err}
|
||||
}
|
||||
|
||||
edgeJob, err := handler.DataStore.EdgeJob().EdgeJob(portainer.EdgeJobID(edgeJobID))
|
||||
if err == portainer.ErrObjectNotFound {
|
||||
return &httperror.HandlerError{http.StatusNotFound, "Unable to find an Edge job with the specified identifier inside the database", err}
|
||||
} else if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an Edge job with the specified identifier inside the database", err}
|
||||
}
|
||||
|
||||
tasks := make([]taskContainer, 0)
|
||||
|
||||
for endpointID, meta := range edgeJob.Endpoints {
|
||||
|
||||
cronTask := taskContainer{
|
||||
ID: fmt.Sprintf("edgejob_task_%d_%d", edgeJob.ID, endpointID),
|
||||
EndpointID: endpointID,
|
||||
LogsStatus: meta.LogsStatus,
|
||||
}
|
||||
|
||||
tasks = append(tasks, cronTask)
|
||||
}
|
||||
|
||||
return response.JSON(w, tasks)
|
||||
}
|
128
api/http/handler/edgejobs/edgejob_update.go
Normal file
128
api/http/handler/edgejobs/edgejob_update.go
Normal file
|
@ -0,0 +1,128 @@
|
|||
package edgejobs
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/asaskevich/govalidator"
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
"github.com/portainer/libhttp/response"
|
||||
"github.com/portainer/portainer/api"
|
||||
)
|
||||
|
||||
type edgeJobUpdatePayload struct {
|
||||
Name *string
|
||||
CronExpression *string
|
||||
Recurring *bool
|
||||
Endpoints []portainer.EndpointID
|
||||
FileContent *string
|
||||
}
|
||||
|
||||
func (payload *edgeJobUpdatePayload) Validate(r *http.Request) error {
|
||||
if payload.Name != nil && !govalidator.Matches(*payload.Name, `^[a-zA-Z0-9][a-zA-Z0-9_.-]+$`) {
|
||||
return errors.New("Invalid Edge job name format. Allowed characters are: [a-zA-Z0-9_.-]")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (handler *Handler) edgeJobUpdate(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
settings, err := handler.DataStore.Settings().Settings()
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusServiceUnavailable, "Unable to retrieve settings", err}
|
||||
}
|
||||
|
||||
if !settings.EnableEdgeComputeFeatures {
|
||||
return &httperror.HandlerError{http.StatusServiceUnavailable, "Edge compute features are disabled", portainer.ErrHostManagementFeaturesDisabled}
|
||||
}
|
||||
|
||||
edgeJobID, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid Edge job identifier route variable", err}
|
||||
}
|
||||
|
||||
var payload edgeJobUpdatePayload
|
||||
err = request.DecodeAndValidateJSONPayload(r, &payload)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err}
|
||||
}
|
||||
|
||||
edgeJob, err := handler.DataStore.EdgeJob().EdgeJob(portainer.EdgeJobID(edgeJobID))
|
||||
if err == portainer.ErrObjectNotFound {
|
||||
return &httperror.HandlerError{http.StatusNotFound, "Unable to find an Edge job with the specified identifier inside the database", err}
|
||||
} else if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an Edge job with the specified identifier inside the database", err}
|
||||
}
|
||||
|
||||
err = handler.updateEdgeSchedule(edgeJob, &payload)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to update Edge job", err}
|
||||
}
|
||||
|
||||
err = handler.DataStore.EdgeJob().UpdateEdgeJob(edgeJob.ID, edgeJob)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist Edge job changes inside the database", err}
|
||||
}
|
||||
|
||||
return response.JSON(w, edgeJob)
|
||||
}
|
||||
|
||||
func (handler *Handler) updateEdgeSchedule(edgeJob *portainer.EdgeJob, payload *edgeJobUpdatePayload) error {
|
||||
if payload.Name != nil {
|
||||
edgeJob.Name = *payload.Name
|
||||
}
|
||||
|
||||
if payload.Endpoints != nil {
|
||||
endpointsMap := map[portainer.EndpointID]portainer.EdgeJobEndpointMeta{}
|
||||
|
||||
for _, endpointID := range payload.Endpoints {
|
||||
endpoint, err := handler.DataStore.Endpoint().Endpoint(endpointID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if endpoint.Type != portainer.EdgeAgentEnvironment {
|
||||
continue
|
||||
}
|
||||
|
||||
if meta, ok := edgeJob.Endpoints[endpointID]; ok {
|
||||
endpointsMap[endpointID] = meta
|
||||
} else {
|
||||
endpointsMap[endpointID] = portainer.EdgeJobEndpointMeta{}
|
||||
}
|
||||
}
|
||||
|
||||
edgeJob.Endpoints = endpointsMap
|
||||
}
|
||||
|
||||
updateVersion := false
|
||||
if payload.CronExpression != nil {
|
||||
edgeJob.CronExpression = *payload.CronExpression
|
||||
updateVersion = true
|
||||
}
|
||||
|
||||
if payload.FileContent != nil {
|
||||
_, err := handler.FileService.StoreEdgeJobFileFromBytes(strconv.Itoa(int(edgeJob.ID)), []byte(*payload.FileContent))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
updateVersion = true
|
||||
}
|
||||
|
||||
if payload.Recurring != nil {
|
||||
edgeJob.Recurring = *payload.Recurring
|
||||
updateVersion = true
|
||||
}
|
||||
|
||||
if updateVersion {
|
||||
edgeJob.Version++
|
||||
}
|
||||
|
||||
for endpointID := range edgeJob.Endpoints {
|
||||
handler.ReverseTunnelService.AddEdgeJob(endpointID, edgeJob)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
47
api/http/handler/edgejobs/handler.go
Normal file
47
api/http/handler/edgejobs/handler.go
Normal file
|
@ -0,0 +1,47 @@
|
|||
package edgejobs
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
)
|
||||
|
||||
// Handler is the HTTP handler used to handle Edge job operations.
|
||||
type Handler struct {
|
||||
*mux.Router
|
||||
DataStore portainer.DataStore
|
||||
FileService portainer.FileService
|
||||
ReverseTunnelService portainer.ReverseTunnelService
|
||||
}
|
||||
|
||||
// NewHandler creates a handler to manage Edge job operations.
|
||||
func NewHandler(bouncer *security.RequestBouncer) *Handler {
|
||||
h := &Handler{
|
||||
Router: mux.NewRouter(),
|
||||
}
|
||||
|
||||
h.Handle("/edge_jobs",
|
||||
bouncer.AdminAccess(bouncer.EdgeComputeOperation(httperror.LoggerHandler(h.edgeJobList)))).Methods(http.MethodGet)
|
||||
h.Handle("/edge_jobs",
|
||||
bouncer.AdminAccess(bouncer.EdgeComputeOperation(httperror.LoggerHandler(h.edgeJobCreate)))).Methods(http.MethodPost)
|
||||
h.Handle("/edge_jobs/{id}",
|
||||
bouncer.AdminAccess(bouncer.EdgeComputeOperation(httperror.LoggerHandler(h.edgeJobInspect)))).Methods(http.MethodGet)
|
||||
h.Handle("/edge_jobs/{id}",
|
||||
bouncer.AdminAccess(bouncer.EdgeComputeOperation(httperror.LoggerHandler(h.edgeJobUpdate)))).Methods(http.MethodPut)
|
||||
h.Handle("/edge_jobs/{id}",
|
||||
bouncer.AdminAccess(bouncer.EdgeComputeOperation(httperror.LoggerHandler(h.edgeJobDelete)))).Methods(http.MethodDelete)
|
||||
h.Handle("/edge_jobs/{id}/file",
|
||||
bouncer.AdminAccess(bouncer.EdgeComputeOperation(httperror.LoggerHandler(h.edgeJobFile)))).Methods(http.MethodGet)
|
||||
h.Handle("/edge_jobs/{id}/tasks",
|
||||
bouncer.AdminAccess(bouncer.EdgeComputeOperation(httperror.LoggerHandler(h.edgeJobTasksList)))).Methods(http.MethodGet)
|
||||
h.Handle("/edge_jobs/{id}/tasks/{taskID}/logs",
|
||||
bouncer.AdminAccess(bouncer.EdgeComputeOperation(httperror.LoggerHandler(h.edgeJobTaskLogsInspect)))).Methods(http.MethodGet)
|
||||
h.Handle("/edge_jobs/{id}/tasks/{taskID}/logs",
|
||||
bouncer.AdminAccess(bouncer.EdgeComputeOperation(httperror.LoggerHandler(h.edgeJobTasksCollect)))).Methods(http.MethodPost)
|
||||
h.Handle("/edge_jobs/{id}/tasks/{taskID}/logs",
|
||||
bouncer.AdminAccess(bouncer.EdgeComputeOperation(httperror.LoggerHandler(h.edgeJobTasksClear)))).Methods(http.MethodDelete)
|
||||
return h
|
||||
}
|
|
@ -25,17 +25,17 @@ func NewHandler(bouncer *security.RequestBouncer) *Handler {
|
|||
requestBouncer: bouncer,
|
||||
}
|
||||
h.Handle("/edge_stacks",
|
||||
bouncer.AdminAccess(httperror.LoggerHandler(h.edgeStackCreate))).Methods(http.MethodPost)
|
||||
bouncer.AdminAccess(bouncer.EdgeComputeOperation(httperror.LoggerHandler(h.edgeStackCreate)))).Methods(http.MethodPost)
|
||||
h.Handle("/edge_stacks",
|
||||
bouncer.AdminAccess(httperror.LoggerHandler(h.edgeStackList))).Methods(http.MethodGet)
|
||||
bouncer.AdminAccess(bouncer.EdgeComputeOperation(httperror.LoggerHandler(h.edgeStackList)))).Methods(http.MethodGet)
|
||||
h.Handle("/edge_stacks/{id}",
|
||||
bouncer.AdminAccess(httperror.LoggerHandler(h.edgeStackInspect))).Methods(http.MethodGet)
|
||||
bouncer.AdminAccess(bouncer.EdgeComputeOperation(httperror.LoggerHandler(h.edgeStackInspect)))).Methods(http.MethodGet)
|
||||
h.Handle("/edge_stacks/{id}",
|
||||
bouncer.AdminAccess(httperror.LoggerHandler(h.edgeStackUpdate))).Methods(http.MethodPut)
|
||||
bouncer.AdminAccess(bouncer.EdgeComputeOperation(httperror.LoggerHandler(h.edgeStackUpdate)))).Methods(http.MethodPut)
|
||||
h.Handle("/edge_stacks/{id}",
|
||||
bouncer.AdminAccess(httperror.LoggerHandler(h.edgeStackDelete))).Methods(http.MethodDelete)
|
||||
bouncer.AdminAccess(bouncer.EdgeComputeOperation(httperror.LoggerHandler(h.edgeStackDelete)))).Methods(http.MethodDelete)
|
||||
h.Handle("/edge_stacks/{id}/file",
|
||||
bouncer.AdminAccess(httperror.LoggerHandler(h.edgeStackFile))).Methods(http.MethodGet)
|
||||
bouncer.AdminAccess(bouncer.EdgeComputeOperation(httperror.LoggerHandler(h.edgeStackFile)))).Methods(http.MethodGet)
|
||||
h.Handle("/edge_stacks/{id}/status",
|
||||
bouncer.PublicAccess(httperror.LoggerHandler(h.edgeStackStatusUpdate))).Methods(http.MethodPut)
|
||||
return h
|
||||
|
|
77
api/http/handler/endpointedge/endpoint_edgejob_logs.go
Normal file
77
api/http/handler/endpointedge/endpoint_edgejob_logs.go
Normal file
|
@ -0,0 +1,77 @@
|
|||
package endpointedge
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
"github.com/portainer/libhttp/response"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
)
|
||||
|
||||
type logsPayload struct {
|
||||
FileContent string
|
||||
}
|
||||
|
||||
func (payload *logsPayload) Validate(r *http.Request) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// POST request on api/endpoints/:id/edge/jobs/:jobID/logs
|
||||
func (handler *Handler) endpointEdgeJobsLogs(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid endpoint identifier route variable", err}
|
||||
}
|
||||
|
||||
endpoint, err := handler.DataStore.Endpoint().Endpoint(portainer.EndpointID(endpointID))
|
||||
if err == portainer.ErrObjectNotFound {
|
||||
return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint with the specified identifier inside the database", err}
|
||||
} else if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err}
|
||||
}
|
||||
|
||||
err = handler.requestBouncer.AuthorizedEdgeEndpointOperation(r, endpoint)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err}
|
||||
}
|
||||
|
||||
edgeJobID, err := request.RetrieveNumericRouteVariableValue(r, "jobID")
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid edge job identifier route variable", err}
|
||||
}
|
||||
|
||||
var payload logsPayload
|
||||
err = request.DecodeAndValidateJSONPayload(r, &payload)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err}
|
||||
}
|
||||
|
||||
edgeJob, err := handler.DataStore.EdgeJob().EdgeJob(portainer.EdgeJobID(edgeJobID))
|
||||
if err == portainer.ErrObjectNotFound {
|
||||
return &httperror.HandlerError{http.StatusNotFound, "Unable to find an edge job with the specified identifier inside the database", err}
|
||||
} else if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an edge job with the specified identifier inside the database", err}
|
||||
}
|
||||
|
||||
err = handler.FileService.StoreEdgeJobTaskLogFileFromBytes(strconv.Itoa(edgeJobID), strconv.Itoa(endpointID), []byte(payload.FileContent))
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to save task log to the filesystem", err}
|
||||
}
|
||||
|
||||
meta := edgeJob.Endpoints[endpoint.ID]
|
||||
meta.CollectLogs = false
|
||||
meta.LogsStatus = portainer.EdgeJobLogsStatusCollected
|
||||
edgeJob.Endpoints[endpoint.ID] = meta
|
||||
|
||||
err = handler.DataStore.EdgeJob().UpdateEdgeJob(edgeJob.ID, edgeJob)
|
||||
|
||||
handler.ReverseTunnelService.AddEdgeJob(endpoint.ID, edgeJob)
|
||||
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist edge job changes to the database", err}
|
||||
}
|
||||
|
||||
return response.JSON(w, nil)
|
||||
}
|
|
@ -13,9 +13,10 @@ import (
|
|||
// Handler is the HTTP handler used to handle edge endpoint operations.
|
||||
type Handler struct {
|
||||
*mux.Router
|
||||
requestBouncer *security.RequestBouncer
|
||||
DataStore portainer.DataStore
|
||||
FileService portainer.FileService
|
||||
requestBouncer *security.RequestBouncer
|
||||
DataStore portainer.DataStore
|
||||
FileService portainer.FileService
|
||||
ReverseTunnelService portainer.ReverseTunnelService
|
||||
}
|
||||
|
||||
// NewHandler creates a handler to manage endpoint operations.
|
||||
|
@ -27,6 +28,7 @@ func NewHandler(bouncer *security.RequestBouncer) *Handler {
|
|||
|
||||
h.Handle("/{id}/edge/stacks/{stackId}",
|
||||
bouncer.PublicAccess(httperror.LoggerHandler(h.endpointEdgeStackInspect))).Methods(http.MethodGet)
|
||||
|
||||
h.Handle("/{id}/edge/jobs/{jobID}/logs",
|
||||
bouncer.PublicAccess(httperror.LoggerHandler(h.endpointEdgeJobsLogs))).Methods(http.MethodPost)
|
||||
return h
|
||||
}
|
||||
|
|
|
@ -1,111 +0,0 @@
|
|||
package endpoints
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"github.com/asaskevich/govalidator"
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
"github.com/portainer/libhttp/response"
|
||||
"github.com/portainer/portainer/api"
|
||||
)
|
||||
|
||||
type endpointJobFromFilePayload struct {
|
||||
Image string
|
||||
File []byte
|
||||
}
|
||||
|
||||
type endpointJobFromFileContentPayload struct {
|
||||
Image string
|
||||
FileContent string
|
||||
}
|
||||
|
||||
func (payload *endpointJobFromFilePayload) Validate(r *http.Request) error {
|
||||
file, _, err := request.RetrieveMultiPartFormFile(r, "File")
|
||||
if err != nil {
|
||||
return portainer.Error("Invalid Script file. Ensure that the file is uploaded correctly")
|
||||
}
|
||||
payload.File = file
|
||||
|
||||
image, err := request.RetrieveMultiPartFormValue(r, "Image", false)
|
||||
if err != nil {
|
||||
return portainer.Error("Invalid image name")
|
||||
}
|
||||
payload.Image = image
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (payload *endpointJobFromFileContentPayload) Validate(r *http.Request) error {
|
||||
if govalidator.IsNull(payload.FileContent) {
|
||||
return portainer.Error("Invalid script file content")
|
||||
}
|
||||
|
||||
if govalidator.IsNull(payload.Image) {
|
||||
return portainer.Error("Invalid image name")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// POST request on /api/endpoints/:id/job?method&nodeName
|
||||
func (handler *Handler) endpointJob(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid endpoint identifier route variable", err}
|
||||
}
|
||||
|
||||
method, err := request.RetrieveQueryParameter(r, "method", false)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid query parameter: method", err}
|
||||
}
|
||||
|
||||
nodeName, _ := request.RetrieveQueryParameter(r, "nodeName", true)
|
||||
|
||||
endpoint, err := handler.DataStore.Endpoint().Endpoint(portainer.EndpointID(endpointID))
|
||||
if err == portainer.ErrObjectNotFound {
|
||||
return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint with the specified identifier inside the database", err}
|
||||
} else if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err}
|
||||
}
|
||||
|
||||
switch method {
|
||||
case "file":
|
||||
return handler.executeJobFromFile(w, r, endpoint, nodeName)
|
||||
case "string":
|
||||
return handler.executeJobFromFileContent(w, r, endpoint, nodeName)
|
||||
}
|
||||
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid value for query parameter: method. Value must be one of: string or file", errors.New(request.ErrInvalidQueryParameter)}
|
||||
}
|
||||
|
||||
func (handler *Handler) executeJobFromFile(w http.ResponseWriter, r *http.Request, endpoint *portainer.Endpoint, nodeName string) *httperror.HandlerError {
|
||||
payload := &endpointJobFromFilePayload{}
|
||||
err := payload.Validate(r)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err}
|
||||
}
|
||||
|
||||
err = handler.JobService.ExecuteScript(endpoint, nodeName, payload.Image, payload.File, nil)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Failed executing job", err}
|
||||
}
|
||||
|
||||
return response.Empty(w)
|
||||
}
|
||||
|
||||
func (handler *Handler) executeJobFromFileContent(w http.ResponseWriter, r *http.Request, endpoint *portainer.Endpoint, nodeName string) *httperror.HandlerError {
|
||||
var payload endpointJobFromFileContentPayload
|
||||
err := request.DecodeAndValidateJSONPayload(r, &payload)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err}
|
||||
}
|
||||
|
||||
err = handler.JobService.ExecuteScript(endpoint, nodeName, payload.Image, []byte(payload.FileContent), nil)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Failed executing job", err}
|
||||
}
|
||||
|
||||
return response.Empty(w)
|
||||
}
|
|
@ -1,6 +1,7 @@
|
|||
package endpoints
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"net/http"
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
|
@ -14,13 +15,21 @@ type stackStatusResponse struct {
|
|||
Version int
|
||||
}
|
||||
|
||||
type edgeJobResponse struct {
|
||||
ID portainer.EdgeJobID `json:"Id"`
|
||||
CollectLogs bool `json:"CollectLogs"`
|
||||
CronExpression string `json:"CronExpression"`
|
||||
Script string `json:"Script"`
|
||||
Version int `json:"Version"`
|
||||
}
|
||||
|
||||
type endpointStatusInspectResponse struct {
|
||||
Status string `json:"status"`
|
||||
Port int `json:"port"`
|
||||
Schedules []portainer.EdgeSchedule `json:"schedules"`
|
||||
CheckinInterval int `json:"checkin"`
|
||||
Credentials string `json:"credentials"`
|
||||
Stacks []stackStatusResponse `json:"stacks"`
|
||||
Status string `json:"status"`
|
||||
Port int `json:"port"`
|
||||
Schedules []edgeJobResponse `json:"schedules"`
|
||||
CheckinInterval int `json:"checkin"`
|
||||
Credentials string `json:"credentials"`
|
||||
Stacks []stackStatusResponse `json:"stacks"`
|
||||
}
|
||||
|
||||
// GET request on /api/endpoints/:id/status
|
||||
|
@ -65,10 +74,30 @@ func (handler *Handler) endpointStatusInspect(w http.ResponseWriter, r *http.Req
|
|||
checkinInterval = endpoint.EdgeCheckinInterval
|
||||
}
|
||||
|
||||
schedules := []edgeJobResponse{}
|
||||
for _, job := range tunnel.Jobs {
|
||||
schedule := edgeJobResponse{
|
||||
ID: job.ID,
|
||||
CronExpression: job.CronExpression,
|
||||
CollectLogs: job.Endpoints[endpoint.ID].CollectLogs,
|
||||
Version: job.Version,
|
||||
}
|
||||
|
||||
file, err := handler.FileService.GetFileContent(job.ScriptPath)
|
||||
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve Edge job script file", err}
|
||||
}
|
||||
|
||||
schedule.Script = base64.RawStdEncoding.EncodeToString(file)
|
||||
|
||||
schedules = append(schedules, schedule)
|
||||
}
|
||||
|
||||
statusResponse := endpointStatusInspectResponse{
|
||||
Status: tunnel.Status,
|
||||
Port: tunnel.Port,
|
||||
Schedules: tunnel.Schedules,
|
||||
Schedules: schedules,
|
||||
CheckinInterval: checkinInterval,
|
||||
Credentials: tunnel.Credentials,
|
||||
}
|
||||
|
|
|
@ -26,7 +26,6 @@ type Handler struct {
|
|||
DataStore portainer.DataStore
|
||||
AuthorizationService *authorization.Service
|
||||
FileService portainer.FileService
|
||||
JobService portainer.JobService
|
||||
ProxyManager *proxy.Manager
|
||||
ReverseTunnelService portainer.ReverseTunnelService
|
||||
Snapshotter portainer.Snapshotter
|
||||
|
@ -55,8 +54,6 @@ func NewHandler(bouncer *security.RequestBouncer) *Handler {
|
|||
bouncer.RestrictedAccess(httperror.LoggerHandler(h.endpointExtensionAdd))).Methods(http.MethodPost)
|
||||
h.Handle("/endpoints/{id}/extensions/{extensionType}",
|
||||
bouncer.RestrictedAccess(httperror.LoggerHandler(h.endpointExtensionRemove))).Methods(http.MethodDelete)
|
||||
h.Handle("/endpoints/{id}/job",
|
||||
bouncer.AdminAccess(httperror.LoggerHandler(h.endpointJob))).Methods(http.MethodPost)
|
||||
h.Handle("/endpoints/{id}/snapshot",
|
||||
bouncer.AdminAccess(httperror.LoggerHandler(h.endpointSnapshot))).Methods(http.MethodPost)
|
||||
h.Handle("/endpoints/{id}/status",
|
||||
|
|
|
@ -4,18 +4,13 @@ import (
|
|||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/portainer/portainer/api/http/handler/auth"
|
||||
"github.com/portainer/portainer/api/http/handler/dockerhub"
|
||||
"github.com/portainer/portainer/api/http/handler/edgegroups"
|
||||
"github.com/portainer/portainer/api/http/handler/edgejobs"
|
||||
"github.com/portainer/portainer/api/http/handler/edgestacks"
|
||||
"github.com/portainer/portainer/api/http/handler/edgetemplates"
|
||||
"github.com/portainer/portainer/api/http/handler/endpointedge"
|
||||
"github.com/portainer/portainer/api/http/handler/support"
|
||||
|
||||
"github.com/portainer/portainer/api/http/handler/schedules"
|
||||
|
||||
"github.com/portainer/portainer/api/http/handler/roles"
|
||||
|
||||
"github.com/portainer/portainer/api/http/handler/auth"
|
||||
"github.com/portainer/portainer/api/http/handler/dockerhub"
|
||||
"github.com/portainer/portainer/api/http/handler/endpointgroups"
|
||||
"github.com/portainer/portainer/api/http/handler/endpointproxy"
|
||||
"github.com/portainer/portainer/api/http/handler/endpoints"
|
||||
|
@ -24,9 +19,11 @@ import (
|
|||
"github.com/portainer/portainer/api/http/handler/motd"
|
||||
"github.com/portainer/portainer/api/http/handler/registries"
|
||||
"github.com/portainer/portainer/api/http/handler/resourcecontrols"
|
||||
"github.com/portainer/portainer/api/http/handler/roles"
|
||||
"github.com/portainer/portainer/api/http/handler/settings"
|
||||
"github.com/portainer/portainer/api/http/handler/stacks"
|
||||
"github.com/portainer/portainer/api/http/handler/status"
|
||||
"github.com/portainer/portainer/api/http/handler/support"
|
||||
"github.com/portainer/portainer/api/http/handler/tags"
|
||||
"github.com/portainer/portainer/api/http/handler/teammemberships"
|
||||
"github.com/portainer/portainer/api/http/handler/teams"
|
||||
|
@ -42,6 +39,7 @@ type Handler struct {
|
|||
AuthHandler *auth.Handler
|
||||
DockerHubHandler *dockerhub.Handler
|
||||
EdgeGroupsHandler *edgegroups.Handler
|
||||
EdgeJobsHandler *edgejobs.Handler
|
||||
EdgeStacksHandler *edgestacks.Handler
|
||||
EdgeTemplatesHandler *edgetemplates.Handler
|
||||
EndpointEdgeHandler *endpointedge.Handler
|
||||
|
@ -54,7 +52,6 @@ type Handler struct {
|
|||
RegistryHandler *registries.Handler
|
||||
ResourceControlHandler *resourcecontrols.Handler
|
||||
RoleHandler *roles.Handler
|
||||
SchedulesHanlder *schedules.Handler
|
||||
SettingsHandler *settings.Handler
|
||||
StackHandler *stacks.Handler
|
||||
StatusHandler *status.Handler
|
||||
|
@ -76,10 +73,12 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||
http.StripPrefix("/api", h.AuthHandler).ServeHTTP(w, r)
|
||||
case strings.HasPrefix(r.URL.Path, "/api/dockerhub"):
|
||||
http.StripPrefix("/api", h.DockerHubHandler).ServeHTTP(w, r)
|
||||
case strings.HasPrefix(r.URL.Path, "/api/edge_stacks"):
|
||||
http.StripPrefix("/api", h.EdgeStacksHandler).ServeHTTP(w, r)
|
||||
case strings.HasPrefix(r.URL.Path, "/api/edge_groups"):
|
||||
http.StripPrefix("/api", h.EdgeGroupsHandler).ServeHTTP(w, r)
|
||||
case strings.HasPrefix(r.URL.Path, "/api/edge_jobs"):
|
||||
http.StripPrefix("/api", h.EdgeJobsHandler).ServeHTTP(w, r)
|
||||
case strings.HasPrefix(r.URL.Path, "/api/edge_stacks"):
|
||||
http.StripPrefix("/api", h.EdgeStacksHandler).ServeHTTP(w, r)
|
||||
case strings.HasPrefix(r.URL.Path, "/api/edge_templates"):
|
||||
http.StripPrefix("/api", h.EdgeTemplatesHandler).ServeHTTP(w, r)
|
||||
case strings.HasPrefix(r.URL.Path, "/api/endpoint_groups"):
|
||||
|
@ -107,8 +106,6 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||
http.StripPrefix("/api", h.ResourceControlHandler).ServeHTTP(w, r)
|
||||
case strings.HasPrefix(r.URL.Path, "/api/roles"):
|
||||
http.StripPrefix("/api", h.RoleHandler).ServeHTTP(w, r)
|
||||
case strings.HasPrefix(r.URL.Path, "/api/schedules"):
|
||||
http.StripPrefix("/api", h.SchedulesHanlder).ServeHTTP(w, r)
|
||||
case strings.HasPrefix(r.URL.Path, "/api/settings"):
|
||||
http.StripPrefix("/api", h.SettingsHandler).ServeHTTP(w, r)
|
||||
case strings.HasPrefix(r.URL.Path, "/api/stacks"):
|
||||
|
|
|
@ -1,43 +0,0 @@
|
|||
package schedules
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
)
|
||||
|
||||
// Handler is the HTTP handler used to handle schedule operations.
|
||||
type Handler struct {
|
||||
*mux.Router
|
||||
DataStore portainer.DataStore
|
||||
FileService portainer.FileService
|
||||
JobService portainer.JobService
|
||||
JobScheduler portainer.JobScheduler
|
||||
ReverseTunnelService portainer.ReverseTunnelService
|
||||
}
|
||||
|
||||
// NewHandler creates a handler to manage schedule operations.
|
||||
func NewHandler(bouncer *security.RequestBouncer) *Handler {
|
||||
h := &Handler{
|
||||
Router: mux.NewRouter(),
|
||||
}
|
||||
|
||||
h.Handle("/schedules",
|
||||
bouncer.AdminAccess(httperror.LoggerHandler(h.scheduleList))).Methods(http.MethodGet)
|
||||
h.Handle("/schedules",
|
||||
bouncer.AdminAccess(httperror.LoggerHandler(h.scheduleCreate))).Methods(http.MethodPost)
|
||||
h.Handle("/schedules/{id}",
|
||||
bouncer.AdminAccess(httperror.LoggerHandler(h.scheduleInspect))).Methods(http.MethodGet)
|
||||
h.Handle("/schedules/{id}",
|
||||
bouncer.AdminAccess(httperror.LoggerHandler(h.scheduleUpdate))).Methods(http.MethodPut)
|
||||
h.Handle("/schedules/{id}",
|
||||
bouncer.AdminAccess(httperror.LoggerHandler(h.scheduleDelete))).Methods(http.MethodDelete)
|
||||
h.Handle("/schedules/{id}/file",
|
||||
bouncer.AdminAccess(httperror.LoggerHandler(h.scheduleFile))).Methods(http.MethodGet)
|
||||
h.Handle("/schedules/{id}/tasks",
|
||||
bouncer.AdminAccess(httperror.LoggerHandler(h.scheduleTasks))).Methods(http.MethodGet)
|
||||
return h
|
||||
}
|
|
@ -1,280 +0,0 @@
|
|||
package schedules
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/asaskevich/govalidator"
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
"github.com/portainer/libhttp/response"
|
||||
"github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/cron"
|
||||
)
|
||||
|
||||
type scheduleCreateFromFilePayload struct {
|
||||
Name string
|
||||
Image string
|
||||
CronExpression string
|
||||
Recurring bool
|
||||
Endpoints []portainer.EndpointID
|
||||
File []byte
|
||||
RetryCount int
|
||||
RetryInterval int
|
||||
}
|
||||
|
||||
type scheduleCreateFromFileContentPayload struct {
|
||||
Name string
|
||||
CronExpression string
|
||||
Recurring bool
|
||||
Image string
|
||||
Endpoints []portainer.EndpointID
|
||||
FileContent string
|
||||
RetryCount int
|
||||
RetryInterval int
|
||||
}
|
||||
|
||||
func (payload *scheduleCreateFromFilePayload) Validate(r *http.Request) error {
|
||||
name, err := request.RetrieveMultiPartFormValue(r, "Name", false)
|
||||
if err != nil {
|
||||
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_.-]")
|
||||
}
|
||||
payload.Name = name
|
||||
|
||||
image, err := request.RetrieveMultiPartFormValue(r, "Image", false)
|
||||
if err != nil {
|
||||
return errors.New("Invalid schedule image")
|
||||
}
|
||||
payload.Image = image
|
||||
|
||||
cronExpression, err := request.RetrieveMultiPartFormValue(r, "CronExpression", false)
|
||||
if err != nil {
|
||||
return errors.New("Invalid cron expression")
|
||||
}
|
||||
payload.CronExpression = cronExpression
|
||||
|
||||
var endpoints []portainer.EndpointID
|
||||
err = request.RetrieveMultiPartFormJSONValue(r, "Endpoints", &endpoints, false)
|
||||
if err != nil {
|
||||
return errors.New("Invalid endpoints")
|
||||
}
|
||||
payload.Endpoints = endpoints
|
||||
|
||||
file, _, err := request.RetrieveMultiPartFormFile(r, "file")
|
||||
if err != nil {
|
||||
return portainer.Error("Invalid script file. Ensure that the file is uploaded correctly")
|
||||
}
|
||||
payload.File = file
|
||||
|
||||
retryCount, _ := request.RetrieveNumericMultiPartFormValue(r, "RetryCount", true)
|
||||
payload.RetryCount = retryCount
|
||||
|
||||
retryInterval, _ := request.RetrieveNumericMultiPartFormValue(r, "RetryInterval", true)
|
||||
payload.RetryInterval = retryInterval
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (payload *scheduleCreateFromFileContentPayload) Validate(r *http.Request) error {
|
||||
if govalidator.IsNull(payload.Name) {
|
||||
return portainer.Error("Invalid schedule name")
|
||||
}
|
||||
|
||||
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_.-]")
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
|
||||
if payload.RetryCount != 0 && payload.RetryInterval == 0 {
|
||||
return portainer.Error("RetryInterval must be set")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// POST /api/schedules?method=file|string
|
||||
func (handler *Handler) scheduleCreate(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
settings, err := handler.DataStore.Settings().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}
|
||||
}
|
||||
|
||||
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 {
|
||||
var payload scheduleCreateFromFileContentPayload
|
||||
err := request.DecodeAndValidateJSONPayload(r, &payload)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err}
|
||||
}
|
||||
|
||||
schedule := handler.createScheduleObjectFromFileContentPayload(&payload)
|
||||
|
||||
err = handler.addAndPersistSchedule(schedule, []byte(payload.FileContent))
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to schedule script job", err}
|
||||
}
|
||||
|
||||
return response.JSON(w, schedule)
|
||||
}
|
||||
|
||||
func (handler *Handler) createScheduleFromFile(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
payload := &scheduleCreateFromFilePayload{}
|
||||
err := payload.Validate(r)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err}
|
||||
}
|
||||
|
||||
schedule := handler.createScheduleObjectFromFilePayload(payload)
|
||||
|
||||
err = handler.addAndPersistSchedule(schedule, payload.File)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to schedule script job", err}
|
||||
}
|
||||
|
||||
return response.JSON(w, schedule)
|
||||
}
|
||||
|
||||
func (handler *Handler) createScheduleObjectFromFilePayload(payload *scheduleCreateFromFilePayload) *portainer.Schedule {
|
||||
scheduleIdentifier := portainer.ScheduleID(handler.DataStore.Schedule().GetNextIdentifier())
|
||||
|
||||
job := &portainer.ScriptExecutionJob{
|
||||
Endpoints: payload.Endpoints,
|
||||
Image: payload.Image,
|
||||
RetryCount: payload.RetryCount,
|
||||
RetryInterval: payload.RetryInterval,
|
||||
}
|
||||
|
||||
schedule := &portainer.Schedule{
|
||||
ID: scheduleIdentifier,
|
||||
Name: payload.Name,
|
||||
CronExpression: payload.CronExpression,
|
||||
Recurring: payload.Recurring,
|
||||
JobType: portainer.ScriptExecutionJobType,
|
||||
ScriptExecutionJob: job,
|
||||
Created: time.Now().Unix(),
|
||||
}
|
||||
|
||||
return schedule
|
||||
}
|
||||
|
||||
func (handler *Handler) createScheduleObjectFromFileContentPayload(payload *scheduleCreateFromFileContentPayload) *portainer.Schedule {
|
||||
scheduleIdentifier := portainer.ScheduleID(handler.DataStore.Schedule().GetNextIdentifier())
|
||||
|
||||
job := &portainer.ScriptExecutionJob{
|
||||
Endpoints: payload.Endpoints,
|
||||
Image: payload.Image,
|
||||
RetryCount: payload.RetryCount,
|
||||
RetryInterval: payload.RetryInterval,
|
||||
}
|
||||
|
||||
schedule := &portainer.Schedule{
|
||||
ID: scheduleIdentifier,
|
||||
Name: payload.Name,
|
||||
CronExpression: payload.CronExpression,
|
||||
Recurring: payload.Recurring,
|
||||
JobType: portainer.ScriptExecutionJobType,
|
||||
ScriptExecutionJob: job,
|
||||
Created: time.Now().Unix(),
|
||||
}
|
||||
|
||||
return schedule
|
||||
}
|
||||
|
||||
func (handler *Handler) addAndPersistSchedule(schedule *portainer.Schedule, file []byte) error {
|
||||
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.DataStore.Endpoint().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
|
||||
|
||||
scriptPath, err := handler.FileService.StoreScheduledJobFileFromBytes(strconv.Itoa(int(schedule.ID)), file)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
schedule.ScriptExecutionJob.ScriptPath = scriptPath
|
||||
|
||||
jobContext := cron.NewScriptExecutionJobContext(handler.JobService, handler.DataStore, handler.FileService)
|
||||
jobRunner := cron.NewScriptExecutionJobRunner(schedule, jobContext)
|
||||
|
||||
err = handler.JobScheduler.ScheduleJob(jobRunner)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return handler.DataStore.Schedule().CreateSchedule(schedule)
|
||||
}
|
|
@ -1,55 +0,0 @@
|
|||
package schedules
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
"github.com/portainer/libhttp/response"
|
||||
"github.com/portainer/portainer/api"
|
||||
)
|
||||
|
||||
func (handler *Handler) scheduleDelete(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
settings, err := handler.DataStore.Settings().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}
|
||||
}
|
||||
|
||||
scheduleID, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid schedule identifier route variable", err}
|
||||
}
|
||||
|
||||
schedule, err := handler.DataStore.Schedule().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.SnapshotJobType || schedule.JobType == portainer.EndpointSyncJobType {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Cannot remove system schedules", errors.New("Cannot remove system schedule")}
|
||||
}
|
||||
|
||||
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}
|
||||
}
|
||||
|
||||
handler.ReverseTunnelService.RemoveSchedule(schedule.ID)
|
||||
|
||||
handler.JobScheduler.UnscheduleJob(schedule.ID)
|
||||
|
||||
err = handler.DataStore.Schedule().DeleteSchedule(portainer.ScheduleID(scheduleID))
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to remove the schedule from the database", err}
|
||||
}
|
||||
|
||||
return response.Empty(w)
|
||||
}
|
|
@ -1,49 +0,0 @@
|
|||
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/api"
|
||||
)
|
||||
|
||||
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 {
|
||||
settings, err := handler.DataStore.Settings().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}
|
||||
}
|
||||
|
||||
scheduleID, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid schedule identifier route variable", err}
|
||||
}
|
||||
|
||||
schedule, err := handler.DataStore.Schedule().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)})
|
||||
}
|
|
@ -1,35 +0,0 @@
|
|||
package schedules
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/portainer/libhttp/response"
|
||||
"github.com/portainer/portainer/api"
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
)
|
||||
|
||||
func (handler *Handler) scheduleInspect(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
settings, err := handler.DataStore.Settings().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}
|
||||
}
|
||||
|
||||
scheduleID, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid schedule identifier route variable", err}
|
||||
}
|
||||
|
||||
schedule, err := handler.DataStore.Schedule().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}
|
||||
}
|
||||
|
||||
return response.JSON(w, schedule)
|
||||
}
|
|
@ -1,27 +0,0 @@
|
|||
package schedules
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/response"
|
||||
"github.com/portainer/portainer/api"
|
||||
)
|
||||
|
||||
// GET request on /api/schedules
|
||||
func (handler *Handler) scheduleList(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
settings, err := handler.DataStore.Settings().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}
|
||||
}
|
||||
|
||||
schedules, err := handler.DataStore.Schedule().Schedules()
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve schedules from the database", err}
|
||||
}
|
||||
|
||||
return response.JSON(w, schedules)
|
||||
}
|
|
@ -1,114 +0,0 @@
|
|||
package schedules
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
"github.com/portainer/libhttp/response"
|
||||
"github.com/portainer/portainer/api"
|
||||
)
|
||||
|
||||
type taskContainer struct {
|
||||
ID string `json:"Id"`
|
||||
EndpointID portainer.EndpointID `json:"EndpointId"`
|
||||
Status string `json:"Status"`
|
||||
Created float64 `json:"Created"`
|
||||
Labels map[string]string `json:"Labels"`
|
||||
Edge bool `json:"Edge"`
|
||||
}
|
||||
|
||||
// GET request on /api/schedules/:id/tasks
|
||||
func (handler *Handler) scheduleTasks(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
settings, err := handler.DataStore.Settings().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}
|
||||
}
|
||||
|
||||
scheduleID, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid schedule identifier route variable", err}
|
||||
}
|
||||
|
||||
schedule, err := handler.DataStore.Schedule().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 schedule tasks", errors.New("This type of schedule do not have any associated tasks")}
|
||||
}
|
||||
|
||||
tasks := make([]taskContainer, 0)
|
||||
|
||||
for _, endpointID := range schedule.ScriptExecutionJob.Endpoints {
|
||||
endpoint, err := handler.DataStore.Endpoint().Endpoint(endpointID)
|
||||
if err == portainer.ErrObjectNotFound {
|
||||
continue
|
||||
} else if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err}
|
||||
}
|
||||
|
||||
endpointTasks, err := extractTasksFromContainerSnasphot(endpoint, schedule.ID)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find extract schedule tasks from endpoint snapshot", err}
|
||||
}
|
||||
|
||||
tasks = append(tasks, endpointTasks...)
|
||||
}
|
||||
|
||||
if schedule.EdgeSchedule != nil {
|
||||
for _, endpointID := range schedule.EdgeSchedule.Endpoints {
|
||||
|
||||
cronTask := taskContainer{
|
||||
ID: fmt.Sprintf("schedule_%d", schedule.EdgeSchedule.ID),
|
||||
EndpointID: endpointID,
|
||||
Edge: true,
|
||||
Status: "",
|
||||
Created: 0,
|
||||
Labels: map[string]string{},
|
||||
}
|
||||
|
||||
tasks = append(tasks, cronTask)
|
||||
}
|
||||
}
|
||||
|
||||
return response.JSON(w, tasks)
|
||||
}
|
||||
|
||||
func extractTasksFromContainerSnasphot(endpoint *portainer.Endpoint, scheduleID portainer.ScheduleID) ([]taskContainer, error) {
|
||||
endpointTasks := make([]taskContainer, 0)
|
||||
if len(endpoint.Snapshots) == 0 {
|
||||
return endpointTasks, nil
|
||||
}
|
||||
|
||||
b, err := json.Marshal(endpoint.Snapshots[0].SnapshotRaw.Containers)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var containers []taskContainer
|
||||
err = json.Unmarshal(b, &containers)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, container := range containers {
|
||||
if container.Labels["io.portainer.schedule.id"] == strconv.Itoa(int(scheduleID)) {
|
||||
container.EndpointID = endpoint.ID
|
||||
container.Edge = false
|
||||
endpointTasks = append(endpointTasks, container)
|
||||
}
|
||||
}
|
||||
|
||||
return endpointTasks, nil
|
||||
}
|
|
@ -1,175 +0,0 @@
|
|||
package schedules
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/asaskevich/govalidator"
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
"github.com/portainer/libhttp/response"
|
||||
"github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/cron"
|
||||
)
|
||||
|
||||
type scheduleUpdatePayload struct {
|
||||
Name *string
|
||||
Image *string
|
||||
CronExpression *string
|
||||
Recurring *bool
|
||||
Endpoints []portainer.EndpointID
|
||||
FileContent *string
|
||||
RetryCount *int
|
||||
RetryInterval *int
|
||||
}
|
||||
|
||||
func (payload *scheduleUpdatePayload) Validate(r *http.Request) error {
|
||||
if payload.Name != nil && !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_.-]")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (handler *Handler) scheduleUpdate(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
settings, err := handler.DataStore.Settings().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}
|
||||
}
|
||||
|
||||
scheduleID, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid schedule identifier route variable", err}
|
||||
}
|
||||
|
||||
var payload scheduleUpdatePayload
|
||||
err = request.DecodeAndValidateJSONPayload(r, &payload)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err}
|
||||
}
|
||||
|
||||
schedule, err := handler.DataStore.Schedule().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}
|
||||
}
|
||||
|
||||
updateJobSchedule := false
|
||||
if schedule.EdgeSchedule != nil {
|
||||
err := handler.updateEdgeSchedule(schedule, &payload)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to update Edge schedule", err}
|
||||
}
|
||||
} else {
|
||||
updateJobSchedule = updateSchedule(schedule, &payload)
|
||||
}
|
||||
|
||||
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.DataStore, handler.FileService)
|
||||
jobRunner := cron.NewScriptExecutionJobRunner(schedule, jobContext)
|
||||
err := handler.JobScheduler.UpdateJobSchedule(jobRunner)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to update job scheduler", err}
|
||||
}
|
||||
}
|
||||
|
||||
err = handler.DataStore.Schedule().UpdateSchedule(portainer.ScheduleID(scheduleID), schedule)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist schedule changes inside the database", err}
|
||||
}
|
||||
|
||||
return response.JSON(w, schedule)
|
||||
}
|
||||
|
||||
func (handler *Handler) updateEdgeSchedule(schedule *portainer.Schedule, payload *scheduleUpdatePayload) error {
|
||||
if payload.Name != nil {
|
||||
schedule.Name = *payload.Name
|
||||
}
|
||||
|
||||
if payload.Endpoints != nil {
|
||||
|
||||
edgeEndpointIDs := make([]portainer.EndpointID, 0)
|
||||
|
||||
for _, ID := range payload.Endpoints {
|
||||
endpoint, err := handler.DataStore.Endpoint().Endpoint(ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if endpoint.Type == portainer.EdgeAgentEnvironment {
|
||||
edgeEndpointIDs = append(edgeEndpointIDs, endpoint.ID)
|
||||
}
|
||||
}
|
||||
|
||||
schedule.EdgeSchedule.Endpoints = edgeEndpointIDs
|
||||
}
|
||||
|
||||
if payload.CronExpression != nil {
|
||||
schedule.EdgeSchedule.CronExpression = *payload.CronExpression
|
||||
schedule.EdgeSchedule.Version++
|
||||
}
|
||||
|
||||
if payload.FileContent != nil {
|
||||
schedule.EdgeSchedule.Script = base64.RawStdEncoding.EncodeToString([]byte(*payload.FileContent))
|
||||
schedule.EdgeSchedule.Version++
|
||||
}
|
||||
|
||||
for _, endpointID := range schedule.EdgeSchedule.Endpoints {
|
||||
handler.ReverseTunnelService.AddSchedule(endpointID, schedule.EdgeSchedule)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func updateSchedule(schedule *portainer.Schedule, payload *scheduleUpdatePayload) bool {
|
||||
updateJobSchedule := false
|
||||
|
||||
if payload.Name != nil {
|
||||
schedule.Name = *payload.Name
|
||||
}
|
||||
|
||||
if payload.Endpoints != nil {
|
||||
schedule.ScriptExecutionJob.Endpoints = payload.Endpoints
|
||||
updateJobSchedule = true
|
||||
}
|
||||
|
||||
if payload.CronExpression != nil {
|
||||
schedule.CronExpression = *payload.CronExpression
|
||||
updateJobSchedule = true
|
||||
}
|
||||
|
||||
if payload.Recurring != nil {
|
||||
schedule.Recurring = *payload.Recurring
|
||||
updateJobSchedule = true
|
||||
}
|
||||
|
||||
if payload.Image != nil {
|
||||
schedule.ScriptExecutionJob.Image = *payload.Image
|
||||
updateJobSchedule = true
|
||||
}
|
||||
|
||||
if payload.RetryCount != nil {
|
||||
schedule.ScriptExecutionJob.RetryCount = *payload.RetryCount
|
||||
updateJobSchedule = true
|
||||
}
|
||||
|
||||
if payload.RetryInterval != nil {
|
||||
schedule.ScriptExecutionJob.RetryInterval = *payload.RetryInterval
|
||||
updateJobSchedule = true
|
||||
}
|
||||
|
||||
return updateJobSchedule
|
||||
}
|
|
@ -8,6 +8,7 @@ import (
|
|||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
"github.com/portainer/portainer/api/internal/snapshot"
|
||||
)
|
||||
|
||||
func hideFields(settings *portainer.Settings) {
|
||||
|
@ -21,9 +22,9 @@ type Handler struct {
|
|||
AuthorizationService *authorization.Service
|
||||
DataStore portainer.DataStore
|
||||
FileService portainer.FileService
|
||||
JobScheduler portainer.JobScheduler
|
||||
JWTService portainer.JWTService
|
||||
LDAPService portainer.LDAPService
|
||||
SnapshotService *snapshot.Service
|
||||
}
|
||||
|
||||
// NewHandler creates a handler to manage settings operations.
|
||||
|
|
|
@ -186,26 +186,11 @@ func (handler *Handler) updateVolumeBrowserSetting(settings *portainer.Settings)
|
|||
func (handler *Handler) updateSnapshotInterval(settings *portainer.Settings, snapshotInterval string) error {
|
||||
settings.SnapshotInterval = snapshotInterval
|
||||
|
||||
schedules, err := handler.DataStore.Schedule().SchedulesByJobType(portainer.SnapshotJobType)
|
||||
err := handler.SnapshotService.SetSnapshotInterval(snapshotInterval)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(schedules) != 0 {
|
||||
snapshotSchedule := schedules[0]
|
||||
snapshotSchedule.CronExpression = "@every " + snapshotInterval
|
||||
|
||||
err := handler.JobScheduler.UpdateSystemJobSchedule(portainer.SnapshotJobType, snapshotSchedule.CronExpression)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = handler.DataStore.Schedule().UpdateSchedule(snapshotSchedule.ID, &snapshotSchedule)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
|
@ -362,3 +362,22 @@ func (bouncer *RequestBouncer) newRestrictedContextRequest(userID portainer.User
|
|||
|
||||
return requestContext, nil
|
||||
}
|
||||
|
||||
// EdgeComputeOperation defines a restriced edge compute operation.
|
||||
// Use of this operation will only be authorized if edgeCompute is enabled in settings
|
||||
func (bouncer *RequestBouncer) EdgeComputeOperation(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
settings, err := bouncer.dataStore.Settings().Settings()
|
||||
if err != nil {
|
||||
httperror.WriteError(w, http.StatusServiceUnavailable, "Unable to retrieve settings", err)
|
||||
return
|
||||
}
|
||||
|
||||
if !settings.EnableEdgeComputeFeatures {
|
||||
httperror.WriteError(w, http.StatusServiceUnavailable, "Edge compute features are disabled", errors.New("Edge compute features are disabled"))
|
||||
return
|
||||
}
|
||||
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@ import (
|
|||
"github.com/portainer/portainer/api/http/handler/edgetemplates"
|
||||
"github.com/portainer/portainer/api/http/handler/endpointedge"
|
||||
"github.com/portainer/portainer/api/http/handler/support"
|
||||
"github.com/portainer/portainer/api/internal/snapshot"
|
||||
|
||||
"github.com/portainer/portainer/api/http/handler/roles"
|
||||
|
||||
|
@ -16,6 +17,7 @@ import (
|
|||
"github.com/portainer/portainer/api/http/handler"
|
||||
"github.com/portainer/portainer/api/http/handler/auth"
|
||||
"github.com/portainer/portainer/api/http/handler/dockerhub"
|
||||
"github.com/portainer/portainer/api/http/handler/edgejobs"
|
||||
"github.com/portainer/portainer/api/http/handler/endpointgroups"
|
||||
"github.com/portainer/portainer/api/http/handler/endpointproxy"
|
||||
"github.com/portainer/portainer/api/http/handler/endpoints"
|
||||
|
@ -24,7 +26,6 @@ import (
|
|||
"github.com/portainer/portainer/api/http/handler/motd"
|
||||
"github.com/portainer/portainer/api/http/handler/registries"
|
||||
"github.com/portainer/portainer/api/http/handler/resourcecontrols"
|
||||
"github.com/portainer/portainer/api/http/handler/schedules"
|
||||
"github.com/portainer/portainer/api/http/handler/settings"
|
||||
"github.com/portainer/portainer/api/http/handler/stacks"
|
||||
"github.com/portainer/portainer/api/http/handler/status"
|
||||
|
@ -54,7 +55,7 @@ type Server struct {
|
|||
ComposeStackManager portainer.ComposeStackManager
|
||||
CryptoService portainer.CryptoService
|
||||
SignatureService portainer.DigitalSignatureService
|
||||
JobScheduler portainer.JobScheduler
|
||||
SnapshotService *snapshot.Service
|
||||
Snapshotter portainer.Snapshotter
|
||||
FileService portainer.FileService
|
||||
DataStore portainer.DataStore
|
||||
|
@ -67,7 +68,6 @@ type Server struct {
|
|||
SSLCert string
|
||||
SSLKey string
|
||||
DockerClientFactory *docker.ClientFactory
|
||||
JobService portainer.JobService
|
||||
}
|
||||
|
||||
// Start starts the HTTP server
|
||||
|
@ -98,6 +98,11 @@ func (server *Server) Start() error {
|
|||
var edgeGroupsHandler = edgegroups.NewHandler(requestBouncer)
|
||||
edgeGroupsHandler.DataStore = server.DataStore
|
||||
|
||||
var edgeJobsHandler = edgejobs.NewHandler(requestBouncer)
|
||||
edgeJobsHandler.DataStore = server.DataStore
|
||||
edgeJobsHandler.FileService = server.FileService
|
||||
edgeJobsHandler.ReverseTunnelService = server.ReverseTunnelService
|
||||
|
||||
var edgeStacksHandler = edgestacks.NewHandler(requestBouncer)
|
||||
edgeStacksHandler.DataStore = server.DataStore
|
||||
edgeStacksHandler.FileService = server.FileService
|
||||
|
@ -110,7 +115,6 @@ func (server *Server) Start() error {
|
|||
endpointHandler.DataStore = server.DataStore
|
||||
endpointHandler.AuthorizationService = authorizationService
|
||||
endpointHandler.FileService = server.FileService
|
||||
endpointHandler.JobService = server.JobService
|
||||
endpointHandler.ProxyManager = proxyManager
|
||||
endpointHandler.ReverseTunnelService = server.ReverseTunnelService
|
||||
endpointHandler.Snapshotter = server.Snapshotter
|
||||
|
@ -118,6 +122,7 @@ func (server *Server) Start() error {
|
|||
var endpointEdgeHandler = endpointedge.NewHandler(requestBouncer)
|
||||
endpointEdgeHandler.DataStore = server.DataStore
|
||||
endpointEdgeHandler.FileService = server.FileService
|
||||
endpointEdgeHandler.ReverseTunnelService = server.ReverseTunnelService
|
||||
|
||||
var endpointGroupHandler = endpointgroups.NewHandler(requestBouncer)
|
||||
endpointGroupHandler.DataStore = server.DataStore
|
||||
|
@ -145,20 +150,13 @@ func (server *Server) Start() error {
|
|||
var resourceControlHandler = resourcecontrols.NewHandler(requestBouncer)
|
||||
resourceControlHandler.DataStore = server.DataStore
|
||||
|
||||
var schedulesHandler = schedules.NewHandler(requestBouncer)
|
||||
schedulesHandler.DataStore = server.DataStore
|
||||
schedulesHandler.FileService = server.FileService
|
||||
schedulesHandler.JobService = server.JobService
|
||||
schedulesHandler.JobScheduler = server.JobScheduler
|
||||
schedulesHandler.ReverseTunnelService = server.ReverseTunnelService
|
||||
|
||||
var settingsHandler = settings.NewHandler(requestBouncer)
|
||||
settingsHandler.AuthorizationService = authorizationService
|
||||
settingsHandler.DataStore = server.DataStore
|
||||
settingsHandler.FileService = server.FileService
|
||||
settingsHandler.JobScheduler = server.JobScheduler
|
||||
settingsHandler.JWTService = server.JWTService
|
||||
settingsHandler.LDAPService = server.LDAPService
|
||||
settingsHandler.SnapshotService = server.SnapshotService
|
||||
|
||||
var stackHandler = stacks.NewHandler(requestBouncer)
|
||||
stackHandler.DataStore = server.DataStore
|
||||
|
@ -207,6 +205,7 @@ func (server *Server) Start() error {
|
|||
AuthHandler: authHandler,
|
||||
DockerHubHandler: dockerHubHandler,
|
||||
EdgeGroupsHandler: edgeGroupsHandler,
|
||||
EdgeJobsHandler: edgeJobsHandler,
|
||||
EdgeStacksHandler: edgeStacksHandler,
|
||||
EdgeTemplatesHandler: edgeTemplatesHandler,
|
||||
EndpointGroupHandler: endpointGroupHandler,
|
||||
|
@ -230,7 +229,6 @@ func (server *Server) Start() error {
|
|||
UserHandler: userHandler,
|
||||
WebSocketHandler: websocketHandler,
|
||||
WebhookHandler: webhookHandler,
|
||||
SchedulesHanlder: schedulesHandler,
|
||||
}
|
||||
|
||||
if server.SSL {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue