mirror of https://github.com/portainer/portainer
feat(schedules): add the ability to list tasks from snapshots (#2458)
* feat(schedules): add the ability to list tasks from snapshots * feat(schedules): update schedules * refactor(schedules): fix linting issuepull/2460/head
parent
a2d9f591a7
commit
64c29f7402
|
@ -141,9 +141,9 @@ func loadSnapshotSystemSchedule(jobScheduler portainer.JobScheduler, snapshotter
|
|||
}
|
||||
|
||||
snapshotJobContext := cron.NewSnapshotJobContext(endpointService, snapshotter)
|
||||
snapshotJobRunner := cron.NewSnapshotJobRunner(snapshotJob, snapshotJobContext)
|
||||
snapshotJobRunner := cron.NewSnapshotJobRunner(snapshotSchedule, snapshotJobContext)
|
||||
|
||||
err = jobScheduler.CreateSchedule(snapshotSchedule, snapshotJobRunner)
|
||||
err = jobScheduler.ScheduleJob(snapshotJobRunner)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -179,9 +179,9 @@ func loadEndpointSyncSystemSchedule(jobScheduler portainer.JobScheduler, schedul
|
|||
}
|
||||
|
||||
endpointSyncJobContext := cron.NewEndpointSyncJobContext(endpointService, *flags.ExternalEndpoints)
|
||||
endpointSyncJobRunner := cron.NewEndpointSyncJobRunner(endpointSyncJob, endpointSyncJobContext)
|
||||
endpointSyncJobRunner := cron.NewEndpointSyncJobRunner(endointSyncSchedule, endpointSyncJobContext)
|
||||
|
||||
err = jobScheduler.CreateSchedule(endointSyncSchedule, endpointSyncJobRunner)
|
||||
err = jobScheduler.ScheduleJob(endpointSyncJobRunner)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -199,9 +199,9 @@ func loadSchedulesFromDatabase(jobScheduler portainer.JobScheduler, jobService p
|
|||
|
||||
if schedule.JobType == portainer.ScriptExecutionJobType {
|
||||
jobContext := cron.NewScriptExecutionJobContext(jobService, endpointService, fileService)
|
||||
jobRunner := cron.NewScriptExecutionJobRunner(schedule.ScriptExecutionJob, jobContext)
|
||||
jobRunner := cron.NewScriptExecutionJobRunner(&schedule, jobContext)
|
||||
|
||||
err = jobScheduler.CreateSchedule(&schedule, jobRunner)
|
||||
err = jobScheduler.ScheduleJob(jobRunner)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
|
@ -11,8 +11,8 @@ import (
|
|||
|
||||
// EndpointSyncJobRunner is used to run a EndpointSyncJob
|
||||
type EndpointSyncJobRunner struct {
|
||||
job *portainer.EndpointSyncJob
|
||||
context *EndpointSyncJobContext
|
||||
schedule *portainer.Schedule
|
||||
context *EndpointSyncJobContext
|
||||
}
|
||||
|
||||
// EndpointSyncJobContext represents the context of execution of a EndpointSyncJob
|
||||
|
@ -30,10 +30,10 @@ func NewEndpointSyncJobContext(endpointService portainer.EndpointService, endpoi
|
|||
}
|
||||
|
||||
// NewEndpointSyncJobRunner returns a new runner that can be scheduled
|
||||
func NewEndpointSyncJobRunner(job *portainer.EndpointSyncJob, context *EndpointSyncJobContext) *EndpointSyncJobRunner {
|
||||
func NewEndpointSyncJobRunner(schedule *portainer.Schedule, context *EndpointSyncJobContext) *EndpointSyncJobRunner {
|
||||
return &EndpointSyncJobRunner{
|
||||
job: job,
|
||||
context: context,
|
||||
schedule: schedule,
|
||||
context: context,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -53,19 +53,9 @@ type fileEndpoint struct {
|
|||
TLSKey string `json:"TLSKey,omitempty"`
|
||||
}
|
||||
|
||||
// GetScheduleID returns the schedule identifier associated to the runner
|
||||
func (runner *EndpointSyncJobRunner) GetScheduleID() portainer.ScheduleID {
|
||||
return runner.job.ScheduleID
|
||||
}
|
||||
|
||||
// SetScheduleID sets the schedule identifier associated to the runner
|
||||
func (runner *EndpointSyncJobRunner) SetScheduleID(ID portainer.ScheduleID) {
|
||||
runner.job.ScheduleID = ID
|
||||
}
|
||||
|
||||
// GetJobType returns the job type associated to the runner
|
||||
func (runner *EndpointSyncJobRunner) GetJobType() portainer.JobType {
|
||||
return portainer.EndpointSyncJobType
|
||||
// GetSchedule returns the schedule associated to the runner
|
||||
func (runner *EndpointSyncJobRunner) GetSchedule() *portainer.Schedule {
|
||||
return runner.schedule
|
||||
}
|
||||
|
||||
// Run triggers the execution of the endpoint synchronization process.
|
||||
|
|
|
@ -9,8 +9,8 @@ import (
|
|||
|
||||
// ScriptExecutionJobRunner is used to run a ScriptExecutionJob
|
||||
type ScriptExecutionJobRunner struct {
|
||||
job *portainer.ScriptExecutionJob
|
||||
context *ScriptExecutionJobContext
|
||||
schedule *portainer.Schedule
|
||||
context *ScriptExecutionJobContext
|
||||
}
|
||||
|
||||
// ScriptExecutionJobContext represents the context of execution of a ScriptExecutionJob
|
||||
|
@ -30,10 +30,10 @@ func NewScriptExecutionJobContext(jobService portainer.JobService, endpointServi
|
|||
}
|
||||
|
||||
// NewScriptExecutionJobRunner returns a new runner that can be scheduled
|
||||
func NewScriptExecutionJobRunner(job *portainer.ScriptExecutionJob, context *ScriptExecutionJobContext) *ScriptExecutionJobRunner {
|
||||
func NewScriptExecutionJobRunner(schedule *portainer.Schedule, context *ScriptExecutionJobContext) *ScriptExecutionJobRunner {
|
||||
return &ScriptExecutionJobRunner{
|
||||
job: job,
|
||||
context: context,
|
||||
schedule: schedule,
|
||||
context: context,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -41,14 +41,14 @@ func NewScriptExecutionJobRunner(job *portainer.ScriptExecutionJob, context *Scr
|
|||
// It will iterate through all the endpoints specified in the context to
|
||||
// execute the script associated to the job.
|
||||
func (runner *ScriptExecutionJobRunner) Run() {
|
||||
scriptFile, err := runner.context.fileService.GetFileContent(runner.job.ScriptPath)
|
||||
scriptFile, err := runner.context.fileService.GetFileContent(runner.schedule.ScriptExecutionJob.ScriptPath)
|
||||
if err != nil {
|
||||
log.Printf("scheduled job error (script execution). Unable to retrieve script file (err=%s)\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
targets := make([]*portainer.Endpoint, 0)
|
||||
for _, endpointID := range runner.job.Endpoints {
|
||||
for _, endpointID := range runner.schedule.ScriptExecutionJob.Endpoints {
|
||||
endpoint, err := runner.context.endpointService.Endpoint(endpointID)
|
||||
if err != nil {
|
||||
log.Printf("scheduled job error (script execution). Unable to retrieve information about endpoint (id=%d) (err=%s)\n", endpointID, err)
|
||||
|
@ -65,7 +65,7 @@ func (runner *ScriptExecutionJobRunner) executeAndRetry(endpoints []*portainer.E
|
|||
retryTargets := make([]*portainer.Endpoint, 0)
|
||||
|
||||
for _, endpoint := range endpoints {
|
||||
err := runner.context.jobService.Execute(endpoint, "", runner.job.Image, script)
|
||||
err := runner.context.jobService.ExecuteScript(endpoint, "", runner.schedule.ScriptExecutionJob.Image, script, runner.schedule)
|
||||
if err == portainer.ErrUnableToPingEndpoint {
|
||||
retryTargets = append(retryTargets, endpoint)
|
||||
} else if err != nil {
|
||||
|
@ -74,26 +74,16 @@ func (runner *ScriptExecutionJobRunner) executeAndRetry(endpoints []*portainer.E
|
|||
}
|
||||
|
||||
retryCount++
|
||||
if retryCount >= runner.job.RetryCount {
|
||||
if retryCount >= runner.schedule.ScriptExecutionJob.RetryCount {
|
||||
return
|
||||
}
|
||||
|
||||
time.Sleep(time.Duration(runner.job.RetryInterval) * time.Second)
|
||||
time.Sleep(time.Duration(runner.schedule.ScriptExecutionJob.RetryInterval) * time.Second)
|
||||
|
||||
runner.executeAndRetry(retryTargets, script, retryCount)
|
||||
}
|
||||
|
||||
// GetScheduleID returns the schedule identifier associated to the runner
|
||||
func (runner *ScriptExecutionJobRunner) GetScheduleID() portainer.ScheduleID {
|
||||
return runner.job.ScheduleID
|
||||
}
|
||||
|
||||
// SetScheduleID sets the schedule identifier associated to the runner
|
||||
func (runner *ScriptExecutionJobRunner) SetScheduleID(ID portainer.ScheduleID) {
|
||||
runner.job.ScheduleID = ID
|
||||
}
|
||||
|
||||
// GetJobType returns the job type associated to the runner
|
||||
func (runner *ScriptExecutionJobRunner) GetJobType() portainer.JobType {
|
||||
return portainer.ScriptExecutionJobType
|
||||
// GetSchedule returns the schedule associated to the runner
|
||||
func (runner *ScriptExecutionJobRunner) GetSchedule() *portainer.Schedule {
|
||||
return runner.schedule
|
||||
}
|
||||
|
|
|
@ -8,8 +8,8 @@ import (
|
|||
|
||||
// SnapshotJobRunner is used to run a SnapshotJob
|
||||
type SnapshotJobRunner struct {
|
||||
job *portainer.SnapshotJob
|
||||
context *SnapshotJobContext
|
||||
schedule *portainer.Schedule
|
||||
context *SnapshotJobContext
|
||||
}
|
||||
|
||||
// SnapshotJobContext represents the context of execution of a SnapshotJob
|
||||
|
@ -27,35 +27,25 @@ func NewSnapshotJobContext(endpointService portainer.EndpointService, snapshotte
|
|||
}
|
||||
|
||||
// NewSnapshotJobRunner returns a new runner that can be scheduled
|
||||
func NewSnapshotJobRunner(job *portainer.SnapshotJob, context *SnapshotJobContext) *SnapshotJobRunner {
|
||||
func NewSnapshotJobRunner(schedule *portainer.Schedule, context *SnapshotJobContext) *SnapshotJobRunner {
|
||||
return &SnapshotJobRunner{
|
||||
job: job,
|
||||
context: context,
|
||||
schedule: schedule,
|
||||
context: context,
|
||||
}
|
||||
}
|
||||
|
||||
// GetScheduleID returns the schedule identifier associated to the runner
|
||||
func (runner *SnapshotJobRunner) GetScheduleID() portainer.ScheduleID {
|
||||
return runner.job.ScheduleID
|
||||
// GetSchedule returns the schedule associated to the runner
|
||||
func (runner *SnapshotJobRunner) GetSchedule() *portainer.Schedule {
|
||||
return runner.schedule
|
||||
}
|
||||
|
||||
// SetScheduleID sets the schedule identifier associated to the runner
|
||||
func (runner *SnapshotJobRunner) SetScheduleID(ID portainer.ScheduleID) {
|
||||
runner.job.ScheduleID = ID
|
||||
}
|
||||
|
||||
// GetJobType returns the job type associated to the runner
|
||||
func (runner *SnapshotJobRunner) GetJobType() portainer.JobType {
|
||||
return portainer.EndpointSyncJobType
|
||||
}
|
||||
|
||||
// Run triggers the execution of the job.
|
||||
// Run triggers the execution of the schedule.
|
||||
// It will iterate through all the endpoints available in the database to
|
||||
// create a snapshot of each one of them.
|
||||
func (runner *SnapshotJobRunner) Run() {
|
||||
endpoints, err := runner.context.endpointService.Endpoints()
|
||||
if err != nil {
|
||||
log.Printf("background job error (endpoint snapshot). Unable to retrieve endpoint list (err=%s)\n", err)
|
||||
log.Printf("background schedule error (endpoint snapshot). Unable to retrieve endpoint list (err=%s)\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -67,7 +57,7 @@ func (runner *SnapshotJobRunner) Run() {
|
|||
snapshot, err := runner.context.snapshotter.CreateSnapshot(&endpoint)
|
||||
endpoint.Status = portainer.EndpointStatusUp
|
||||
if err != nil {
|
||||
log.Printf("background job error (endpoint snapshot). Unable to create snapshot (endpoint=%s, URL=%s) (err=%s)\n", endpoint.Name, endpoint.URL, err)
|
||||
log.Printf("background schedule error (endpoint snapshot). Unable to create snapshot (endpoint=%s, URL=%s) (err=%s)\n", endpoint.Name, endpoint.URL, err)
|
||||
endpoint.Status = portainer.EndpointStatusDown
|
||||
}
|
||||
|
||||
|
@ -77,7 +67,7 @@ func (runner *SnapshotJobRunner) Run() {
|
|||
|
||||
err = runner.context.endpointService.UpdateEndpoint(endpoint.ID, &endpoint)
|
||||
if err != nil {
|
||||
log.Printf("background job error (endpoint snapshot). Unable to update endpoint (endpoint=%s, URL=%s) (err=%s)\n", endpoint.Name, endpoint.URL, err)
|
||||
log.Printf("background schedule error (endpoint snapshot). Unable to update endpoint (endpoint=%s, URL=%s) (err=%s)\n", endpoint.Name, endpoint.URL, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,31 +17,25 @@ func NewJobScheduler() *JobScheduler {
|
|||
}
|
||||
}
|
||||
|
||||
// CreateSchedule schedules the execution of a job via a runner
|
||||
func (scheduler *JobScheduler) CreateSchedule(schedule *portainer.Schedule, runner portainer.JobRunner) error {
|
||||
runner.SetScheduleID(schedule.ID)
|
||||
return scheduler.cron.AddJob(schedule.CronExpression, runner)
|
||||
// ScheduleJob schedules the execution of a job via a runner
|
||||
func (scheduler *JobScheduler) ScheduleJob(runner portainer.JobRunner) error {
|
||||
return scheduler.cron.AddJob(runner.GetSchedule().CronExpression, runner)
|
||||
}
|
||||
|
||||
// UpdateSchedule updates a specific scheduled job by re-creating a new cron
|
||||
// UpdateSystemJobSchedule updates the first occurence of the specified
|
||||
// scheduled job based on the specified job type.
|
||||
// It does so by re-creating a new cron
|
||||
// and adding all the existing jobs. It will then re-schedule the new job
|
||||
// via the specified JobRunner parameter.
|
||||
// with the update cron expression passed in parameter.
|
||||
// NOTE: the cron library do not support updating schedules directly
|
||||
// hence the work-around
|
||||
func (scheduler *JobScheduler) UpdateSchedule(schedule *portainer.Schedule, runner portainer.JobRunner) error {
|
||||
func (scheduler *JobScheduler) UpdateSystemJobSchedule(jobType portainer.JobType, newCronExpression string) error {
|
||||
cronEntries := scheduler.cron.Entries()
|
||||
newCron := cron.New()
|
||||
|
||||
for _, entry := range cronEntries {
|
||||
|
||||
if entry.Job.(portainer.JobRunner).GetScheduleID() == schedule.ID {
|
||||
|
||||
var jobRunner cron.Job = runner
|
||||
if entry.Job.(portainer.JobRunner).GetJobType() == portainer.SnapshotJobType {
|
||||
jobRunner = entry.Job
|
||||
}
|
||||
|
||||
err := newCron.AddJob(schedule.CronExpression, jobRunner)
|
||||
if entry.Job.(portainer.JobRunner).GetSchedule().JobType == jobType {
|
||||
err := newCron.AddJob(newCronExpression, entry.Job)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -56,17 +50,50 @@ func (scheduler *JobScheduler) UpdateSchedule(schedule *portainer.Schedule, runn
|
|||
return nil
|
||||
}
|
||||
|
||||
// RemoveSchedule remove a scheduled job by re-creating a new cron
|
||||
// and adding all the existing jobs except for the one specified via scheduleID.
|
||||
// NOTE: the cron library do not support removing schedules directly
|
||||
// UpdateJobSchedule updates a specific scheduled job by re-creating a new cron
|
||||
// and adding all the existing jobs. It will then re-schedule the new job
|
||||
// via the specified JobRunner parameter.
|
||||
// NOTE: the cron library do not support updating schedules directly
|
||||
// hence the work-around
|
||||
func (scheduler *JobScheduler) RemoveSchedule(scheduleID portainer.ScheduleID) {
|
||||
func (scheduler *JobScheduler) UpdateJobSchedule(runner portainer.JobRunner) error {
|
||||
cronEntries := scheduler.cron.Entries()
|
||||
newCron := cron.New()
|
||||
|
||||
for _, entry := range cronEntries {
|
||||
|
||||
if entry.Job.(portainer.JobRunner).GetScheduleID() == scheduleID {
|
||||
if entry.Job.(portainer.JobRunner).GetSchedule().ID == runner.GetSchedule().ID {
|
||||
|
||||
var jobRunner cron.Job = runner
|
||||
if entry.Job.(portainer.JobRunner).GetSchedule().JobType == portainer.SnapshotJobType {
|
||||
jobRunner = entry.Job
|
||||
}
|
||||
|
||||
err := newCron.AddJob(runner.GetSchedule().CronExpression, jobRunner)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
newCron.Schedule(entry.Schedule, entry.Job)
|
||||
}
|
||||
|
||||
scheduler.cron.Stop()
|
||||
scheduler.cron = newCron
|
||||
scheduler.cron.Start()
|
||||
return nil
|
||||
}
|
||||
|
||||
// UnscheduleJob remove a scheduled job by re-creating a new cron
|
||||
// and adding all the existing jobs except for the one specified via scheduleID.
|
||||
// NOTE: the cron library do not support removing schedules directly
|
||||
// hence the work-around
|
||||
func (scheduler *JobScheduler) UnscheduleJob(scheduleID portainer.ScheduleID) {
|
||||
cronEntries := scheduler.cron.Entries()
|
||||
newCron := cron.New()
|
||||
|
||||
for _, entry := range cronEntries {
|
||||
|
||||
if entry.Job.(portainer.JobRunner).GetSchedule().ID == scheduleID {
|
||||
continue
|
||||
}
|
||||
|
||||
|
|
|
@ -18,24 +18,25 @@ import (
|
|||
|
||||
// JobService represents a service that handles the execution of jobs
|
||||
type JobService struct {
|
||||
DockerClientFactory *ClientFactory
|
||||
dockerClientFactory *ClientFactory
|
||||
}
|
||||
|
||||
// NewJobService returns a pointer to a new job service
|
||||
func NewJobService(dockerClientFactory *ClientFactory) *JobService {
|
||||
return &JobService{
|
||||
DockerClientFactory: dockerClientFactory,
|
||||
dockerClientFactory: dockerClientFactory,
|
||||
}
|
||||
}
|
||||
|
||||
// Execute will execute a script on the endpoint host with the supplied image as a container
|
||||
func (service *JobService) Execute(endpoint *portainer.Endpoint, nodeName, image string, script []byte) error {
|
||||
// ExecuteScript will leverage a privileged container to execute a script against the specified endpoint/nodename.
|
||||
// It will copy the script content specified as a parameter inside a container based on the specified image and execute it.
|
||||
func (service *JobService) ExecuteScript(endpoint *portainer.Endpoint, nodeName, image string, script []byte, schedule *portainer.Schedule) error {
|
||||
buffer, err := archive.TarFileInBuffer(script, "script.sh", 0700)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cli, err := service.DockerClientFactory.CreateClient(endpoint, nodeName)
|
||||
cli, err := service.dockerClientFactory.CreateClient(endpoint, nodeName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -64,6 +65,10 @@ func (service *JobService) Execute(endpoint *portainer.Endpoint, nodeName, image
|
|||
Cmd: strslice.StrSlice([]string{"sh", "/tmp/script.sh"}),
|
||||
}
|
||||
|
||||
if schedule != nil {
|
||||
containerConfig.Labels["io.portainer.schedule.id"] = strconv.Itoa(int(schedule.ID))
|
||||
}
|
||||
|
||||
hostConfig := &container.HostConfig{
|
||||
Binds: []string{"/:/host", "/etc:/etc:ro", "/usr:/usr:ro", "/run:/run:ro", "/sbin:/sbin:ro", "/var:/var:ro"},
|
||||
NetworkMode: "host",
|
||||
|
@ -77,6 +82,13 @@ func (service *JobService) Execute(endpoint *portainer.Endpoint, nodeName, image
|
|||
return err
|
||||
}
|
||||
|
||||
if schedule != nil {
|
||||
err = cli.ContainerRename(context.Background(), body.ID, endpoint.Name+"_"+schedule.Name+"_"+body.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
copyOptions := types.CopyToContainerOptions{}
|
||||
err = cli.CopyToContainer(context.Background(), body.ID, "/tmp", bytes.NewReader(buffer), copyOptions)
|
||||
if err != nil {
|
||||
|
@ -84,12 +96,7 @@ func (service *JobService) Execute(endpoint *portainer.Endpoint, nodeName, image
|
|||
}
|
||||
|
||||
startOptions := types.ContainerStartOptions{}
|
||||
err = cli.ContainerStart(context.Background(), body.ID, startOptions)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
return cli.ContainerStart(context.Background(), body.ID, startOptions)
|
||||
}
|
||||
|
||||
func pullImage(cli *client.Client, image string) error {
|
||||
|
|
|
@ -92,7 +92,7 @@ func (handler *Handler) executeJobFromFile(w http.ResponseWriter, r *http.Reques
|
|||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err}
|
||||
}
|
||||
|
||||
err = handler.JobService.Execute(endpoint, nodeName, payload.Image, payload.File)
|
||||
err = handler.JobService.ExecuteScript(endpoint, nodeName, payload.Image, payload.File, nil)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Failed executing job", err}
|
||||
}
|
||||
|
@ -107,7 +107,7 @@ func (handler *Handler) executeJobFromFileContent(w http.ResponseWriter, r *http
|
|||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err}
|
||||
}
|
||||
|
||||
err = handler.JobService.Execute(endpoint, nodeName, payload.Image, []byte(payload.FileContent))
|
||||
err = handler.JobService.ExecuteScript(endpoint, nodeName, payload.Image, []byte(payload.FileContent), nil)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Failed executing job", err}
|
||||
}
|
||||
|
|
|
@ -37,5 +37,7 @@ func NewHandler(bouncer *security.RequestBouncer) *Handler {
|
|||
bouncer.AdministratorAccess(httperror.LoggerHandler(h.scheduleDelete))).Methods(http.MethodDelete)
|
||||
h.Handle("/schedules/{id}/file",
|
||||
bouncer.AdministratorAccess(httperror.LoggerHandler(h.scheduleFile))).Methods(http.MethodGet)
|
||||
h.Handle("/schedules/{id}/tasks",
|
||||
bouncer.AdministratorAccess(httperror.LoggerHandler(h.scheduleTasks))).Methods(http.MethodGet)
|
||||
return h
|
||||
}
|
||||
|
|
|
@ -158,9 +158,9 @@ func (handler *Handler) createScheduleObjectFromFilePayload(payload *scheduleCre
|
|||
scheduleIdentifier := portainer.ScheduleID(handler.ScheduleService.GetNextIdentifier())
|
||||
|
||||
job := &portainer.ScriptExecutionJob{
|
||||
Endpoints: payload.Endpoints,
|
||||
Image: payload.Image,
|
||||
ScheduleID: scheduleIdentifier,
|
||||
Endpoints: payload.Endpoints,
|
||||
Image: payload.Image,
|
||||
// ScheduleID: scheduleIdentifier,
|
||||
RetryCount: payload.RetryCount,
|
||||
RetryInterval: payload.RetryInterval,
|
||||
}
|
||||
|
@ -181,9 +181,9 @@ func (handler *Handler) createScheduleObjectFromFileContentPayload(payload *sche
|
|||
scheduleIdentifier := portainer.ScheduleID(handler.ScheduleService.GetNextIdentifier())
|
||||
|
||||
job := &portainer.ScriptExecutionJob{
|
||||
Endpoints: payload.Endpoints,
|
||||
Image: payload.Image,
|
||||
ScheduleID: scheduleIdentifier,
|
||||
Endpoints: payload.Endpoints,
|
||||
Image: payload.Image,
|
||||
// ScheduleID: scheduleIdentifier,
|
||||
RetryCount: payload.RetryCount,
|
||||
RetryInterval: payload.RetryInterval,
|
||||
}
|
||||
|
@ -209,9 +209,9 @@ func (handler *Handler) addAndPersistSchedule(schedule *portainer.Schedule, file
|
|||
schedule.ScriptExecutionJob.ScriptPath = scriptPath
|
||||
|
||||
jobContext := cron.NewScriptExecutionJobContext(handler.JobService, handler.EndpointService, handler.FileService)
|
||||
jobRunner := cron.NewScriptExecutionJobRunner(schedule.ScriptExecutionJob, jobContext)
|
||||
jobRunner := cron.NewScriptExecutionJobRunner(schedule, jobContext)
|
||||
|
||||
err = handler.JobScheduler.CreateSchedule(schedule, jobRunner)
|
||||
err = handler.JobScheduler.ScheduleJob(jobRunner)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
|
@ -34,6 +34,8 @@ func (handler *Handler) scheduleDelete(w http.ResponseWriter, r *http.Request) *
|
|||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to remove the files associated to the schedule on the filesystem", err}
|
||||
}
|
||||
|
||||
handler.JobScheduler.UnscheduleJob(schedule.ID)
|
||||
|
||||
err = handler.ScheduleService.DeleteSchedule(portainer.ScheduleID(scheduleID))
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to remove the schedule from the database", err}
|
||||
|
|
|
@ -0,0 +1,87 @@
|
|||
package schedules
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
"github.com/portainer/libhttp/response"
|
||||
"github.com/portainer/portainer"
|
||||
)
|
||||
|
||||
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"`
|
||||
}
|
||||
|
||||
// GET request on /api/schedules/:id/tasks
|
||||
func (handler *Handler) scheduleTasks(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
scheduleID, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid schedule identifier route variable", err}
|
||||
}
|
||||
|
||||
schedule, err := handler.ScheduleService.Schedule(portainer.ScheduleID(scheduleID))
|
||||
if err == portainer.ErrObjectNotFound {
|
||||
return &httperror.HandlerError{http.StatusNotFound, "Unable to find a schedule with the specified identifier inside the database", err}
|
||||
} else if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a schedule with the specified identifier inside the database", err}
|
||||
}
|
||||
|
||||
if schedule.JobType != portainer.ScriptExecutionJobType {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Unable to retrieve 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.EndpointService.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...)
|
||||
}
|
||||
|
||||
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
|
||||
endpointTasks = append(endpointTasks, container)
|
||||
}
|
||||
}
|
||||
|
||||
return endpointTasks, nil
|
||||
}
|
|
@ -56,8 +56,8 @@ func (handler *Handler) scheduleUpdate(w http.ResponseWriter, r *http.Request) *
|
|||
|
||||
if updateJobSchedule {
|
||||
jobContext := cron.NewScriptExecutionJobContext(handler.JobService, handler.EndpointService, handler.FileService)
|
||||
jobRunner := cron.NewScriptExecutionJobRunner(schedule.ScriptExecutionJob, jobContext)
|
||||
err := handler.JobScheduler.UpdateSchedule(schedule, jobRunner)
|
||||
jobRunner := cron.NewScriptExecutionJobRunner(schedule, jobContext)
|
||||
err := handler.JobScheduler.UpdateJobSchedule(jobRunner)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to update job scheduler", err}
|
||||
}
|
||||
|
|
|
@ -108,7 +108,12 @@ func (handler *Handler) updateSnapshotInterval(settings *portainer.Settings, sna
|
|||
snapshotSchedule := schedules[0]
|
||||
snapshotSchedule.CronExpression = "@every " + snapshotInterval
|
||||
|
||||
err := handler.JobScheduler.UpdateSchedule(&snapshotSchedule, nil)
|
||||
err := handler.JobScheduler.UpdateSystemJobSchedule(portainer.SnapshotJobType, snapshotSchedule.CronExpression)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = handler.ScheduleService.UpdateSchedule(snapshotSchedule.ID, &snapshotSchedule)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
|
@ -228,7 +228,6 @@ type (
|
|||
|
||||
// ScriptExecutionJob represents a scheduled job that can execute a script via a privileged container
|
||||
ScriptExecutionJob struct {
|
||||
ScheduleID ScheduleID `json:"ScheduleId"`
|
||||
Endpoints []EndpointID
|
||||
Image string
|
||||
ScriptPath string
|
||||
|
@ -237,14 +236,10 @@ type (
|
|||
}
|
||||
|
||||
// SnapshotJob represents a scheduled job that can create endpoint snapshots
|
||||
SnapshotJob struct {
|
||||
ScheduleID ScheduleID `json:"ScheduleId"`
|
||||
}
|
||||
SnapshotJob struct{}
|
||||
|
||||
// EndpointSyncJob represents a scheduled job that synchronize endpoints based on an external file
|
||||
EndpointSyncJob struct {
|
||||
ScheduleID ScheduleID `json:"ScheduleId"`
|
||||
}
|
||||
EndpointSyncJob struct{}
|
||||
|
||||
// Schedule represents a scheduled job.
|
||||
// It only contains a pointer to one of the JobRunner implementations
|
||||
|
@ -668,18 +663,17 @@ type (
|
|||
|
||||
// JobScheduler represents a service to run jobs on a periodic basis
|
||||
JobScheduler interface {
|
||||
CreateSchedule(schedule *Schedule, runner JobRunner) error
|
||||
UpdateSchedule(schedule *Schedule, runner JobRunner) error
|
||||
RemoveSchedule(ID ScheduleID)
|
||||
ScheduleJob(runner JobRunner) error
|
||||
UpdateJobSchedule(runner JobRunner) error
|
||||
UpdateSystemJobSchedule(jobType JobType, newCronExpression string) error
|
||||
UnscheduleJob(ID ScheduleID)
|
||||
Start()
|
||||
}
|
||||
|
||||
// JobRunner represents a service that can be used to run a job
|
||||
JobRunner interface {
|
||||
Run()
|
||||
GetScheduleID() ScheduleID
|
||||
SetScheduleID(ID ScheduleID)
|
||||
GetJobType() JobType
|
||||
GetSchedule() *Schedule
|
||||
}
|
||||
|
||||
// Snapshotter represents a service used to create endpoint snapshots
|
||||
|
@ -710,7 +704,7 @@ type (
|
|||
|
||||
// JobService represents a service to manage job execution on hosts
|
||||
JobService interface {
|
||||
Execute(endpoint *Endpoint, nodeName, image string, script []byte) error
|
||||
ExecuteScript(endpoint *Endpoint, nodeName, image string, script []byte, schedule *Schedule) error
|
||||
}
|
||||
)
|
||||
|
||||
|
|
|
@ -71,7 +71,7 @@
|
|||
<tr dir-paginate="item in ($ctrl.state.filteredDataSet = ($ctrl.dataset | filter: $ctrl.applyFilters | filter:$ctrl.state.textFilter | orderBy:$ctrl.state.orderBy:$ctrl.state.reverseOrder | itemsPerPage: $ctrl.state.paginatedItemLimit))">
|
||||
<td>
|
||||
<a ui-sref="docker.containers.container.logs({ id: item.Id, nodeName: item.NodeName })" title="{{ item.Id }}">
|
||||
{{ item.Id | truncate: 32}}</a>
|
||||
{{ item | containername }}</a>
|
||||
</td>
|
||||
<td>
|
||||
<span ng-if="['starting','healthy','unhealthy'].indexOf(item.Status) !== -1" class="label label-{{ item.Status|containerstatusbadge }} interactive"
|
||||
|
|
|
@ -0,0 +1,97 @@
|
|||
<div class="row">
|
||||
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
|
||||
<div class="datatable">
|
||||
<rd-widget>
|
||||
<rd-widget-body classes="no-padding">
|
||||
<div class="toolBar">
|
||||
<div class="toolBarTitle">
|
||||
<i class="fa" ng-class="$ctrl.titleIcon" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||
{{ $ctrl.titleText }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="searchBar">
|
||||
<i class="fa fa-search searchIcon" aria-hidden="true"></i>
|
||||
<input type="text" class="searchInput" ng-model="$ctrl.state.textFilter" placeholder="Search..." auto-focus>
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover table-filters nowrap-cells">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
<a ng-click="$ctrl.changeOrderBy('Endpoint')">
|
||||
<i class="fa fa-sort-alpha-down" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Endpoint' && !$ctrl.state.reverseOrder"></i>
|
||||
<i class="fa fa-sort-alpha-up" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Endpoint' && $ctrl.state.reverseOrder"></i>
|
||||
Endpoint
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a ng-click="$ctrl.changeOrderBy('Id')">
|
||||
Id
|
||||
<i class="fa fa-sort-alpha-down" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Id' && !$ctrl.state.reverseOrder"></i>
|
||||
<i class="fa fa-sort-alpha-up" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Id' && $ctrl.state.reverseOrder"></i>
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a ng-click="$ctrl.changeOrderBy('Status')">
|
||||
State
|
||||
<i class="fa fa-sort-alpha-down" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Status' && !$ctrl.state.reverseOrder"></i>
|
||||
<i class="fa fa-sort-alpha-up" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Status' && $ctrl.state.reverseOrder"></i>
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a ng-click="$ctrl.changeOrderBy('Created')">
|
||||
<i class="fa fa-sort-alpha-down" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Created' && !$ctrl.state.reverseOrder"></i>
|
||||
<i class="fa fa-sort-alpha-up" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Created' && $ctrl.state.reverseOrder"></i>
|
||||
Created
|
||||
</a>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr dir-paginate="item in ($ctrl.state.filteredDataSet = ($ctrl.dataset | filter: $ctrl.applyFilters | filter:$ctrl.state.textFilter | orderBy:$ctrl.state.orderBy:$ctrl.state.reverseOrder | itemsPerPage: $ctrl.state.paginatedItemLimit))">
|
||||
<td>
|
||||
{{ item.Endpoint.Name }}
|
||||
</td>
|
||||
<td>
|
||||
<a ng-click="$ctrl.goToContainerLogs(item.EndpointId, item.Id)">{{ item.Id | truncate: 32 }}</a>
|
||||
</td>
|
||||
<td>
|
||||
<span class="label label-{{ item.Status|containerstatusbadge }}">{{ item.Status }}</span>
|
||||
</td>
|
||||
<td>
|
||||
{{ item.Created | getisodatefromtimestamp}}
|
||||
</td>
|
||||
</tr>
|
||||
<tr ng-if="!$ctrl.dataset">
|
||||
<td colspan="9" class="text-center text-muted">Loading...</td>
|
||||
</tr>
|
||||
<tr ng-if="$ctrl.state.filteredDataSet.length === 0">
|
||||
<td colspan="9" class="text-center text-muted">No tasks available.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="footer" ng-if="$ctrl.dataset">
|
||||
<div class="paginationControls">
|
||||
<form class="form-inline">
|
||||
<span class="limitSelector">
|
||||
<span style="margin-right: 5px;">
|
||||
Items per page
|
||||
</span>
|
||||
<select class="form-control" ng-model="$ctrl.state.paginatedItemLimit" ng-change="$ctrl.changePaginationLimit()">
|
||||
<option value="0">All</option>
|
||||
<option value="10">10</option>
|
||||
<option value="25">25</option>
|
||||
<option value="50">50</option>
|
||||
<option value="100">100</option>
|
||||
</select>
|
||||
</span>
|
||||
<dir-pagination-controls max-size="5"></dir-pagination-controls>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,13 @@
|
|||
angular.module('portainer.docker').component('scheduleTasksDatatable', {
|
||||
templateUrl: 'app/portainer/components/datatables/schedule-tasks-datatable/scheduleTasksDatatable.html',
|
||||
controller: 'GenericDatatableController',
|
||||
bindings: {
|
||||
titleText: '@',
|
||||
titleIcon: '@',
|
||||
dataset: '<',
|
||||
tableKey: '@',
|
||||
orderBy: '@',
|
||||
reverseOrder: '<',
|
||||
goToContainerLogs: '<'
|
||||
}
|
||||
});
|
|
@ -33,6 +33,13 @@ function ScriptExecutionJobModel(data) {
|
|||
this.RetryInterval = data.RetryInterval;
|
||||
}
|
||||
|
||||
function ScriptExecutionTaskModel(data) {
|
||||
this.Id = data.Id;
|
||||
this.EndpointId = data.EndpointId;
|
||||
this.Status = createStatus(data.Status);
|
||||
this.Created = data.Created;
|
||||
}
|
||||
|
||||
function ScheduleCreateRequest(model) {
|
||||
this.Name = model.Name;
|
||||
this.CronExpression = model.CronExpression;
|
||||
|
|
|
@ -8,6 +8,7 @@ function SchedulesFactory($resource, API_ENDPOINT_SCHEDULES) {
|
|||
get: { method: 'GET', params: { id: '@id' } },
|
||||
update: { method: 'PUT', params: { id: '@id' } },
|
||||
remove: { method: 'DELETE', params: { id: '@id'} },
|
||||
file: { method: 'GET', params: { id : '@id', action: 'file' } }
|
||||
file: { method: 'GET', params: { id : '@id', action: 'file' } },
|
||||
tasks: { method: 'GET', isArray: true, params: { id : '@id', action: 'tasks' } }
|
||||
});
|
||||
}]);
|
||||
|
|
|
@ -36,6 +36,23 @@ function ScheduleService($q, Schedules, FileUploadService) {
|
|||
return deferred.promise;
|
||||
};
|
||||
|
||||
service.scriptExecutionTasks = function(scheduleId) {
|
||||
var deferred = $q.defer();
|
||||
|
||||
Schedules.tasks({ id: scheduleId }).$promise
|
||||
.then(function success(data) {
|
||||
var tasks = data.map(function (item) {
|
||||
return new ScriptExecutionTaskModel(item);
|
||||
});
|
||||
deferred.resolve(tasks);
|
||||
})
|
||||
.catch(function error(err) {
|
||||
deferred.reject({ msg: 'Unable to retrieve tasks associated to the schedule', err: err });
|
||||
});
|
||||
|
||||
return deferred.promise;
|
||||
};
|
||||
|
||||
service.createScheduleFromFileContent = function(model) {
|
||||
var payload = new ScheduleCreateRequest(model);
|
||||
return Schedules.create({ method: 'string' }, payload).$promise;
|
||||
|
|
|
@ -13,15 +13,53 @@
|
|||
<div class="col-sm-12">
|
||||
<rd-widget>
|
||||
<rd-widget-body>
|
||||
<schedule-form
|
||||
ng-if="schedule"
|
||||
model="schedule"
|
||||
endpoints="endpoints"
|
||||
groups="groups"
|
||||
form-action="update"
|
||||
form-action-label="Update schedule"
|
||||
action-in-progress="state.actionInProgress"
|
||||
></schedule-form>
|
||||
|
||||
<uib-tabset active="state.activeTab">
|
||||
<uib-tab index="0">
|
||||
<uib-tab-heading>
|
||||
<i class="fa fa-wrench" aria-hidden="true"></i> Configuration
|
||||
</uib-tab-heading>
|
||||
|
||||
<schedule-form
|
||||
ng-if="schedule"
|
||||
model="schedule"
|
||||
endpoints="endpoints"
|
||||
groups="groups"
|
||||
form-action="update"
|
||||
form-action-label="Update schedule"
|
||||
action-in-progress="state.actionInProgress"
|
||||
></schedule-form>
|
||||
</uib-tab>
|
||||
|
||||
|
||||
<uib-tab index="1">
|
||||
<uib-tab-heading>
|
||||
<i class="fa fa-tasks" aria-hidden="true"></i> Tasks
|
||||
</uib-tab-heading>
|
||||
|
||||
<div class="col-sm-12 form-section-title">
|
||||
Information
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<span class="col-sm-12 text-muted small">
|
||||
Tasks are retrieved across all endpoints via snapshots. Data available in this view might not be up-to-date.
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-12 form-section-title" style="margin-bottom: 20px;">
|
||||
Tasks
|
||||
</div>
|
||||
<schedule-tasks-datatable
|
||||
ng-if="tasks"
|
||||
title-text="Tasks" title-icon="fa-tasks"
|
||||
dataset="tasks"
|
||||
table-key="schedule-tasks"
|
||||
order-by="Status" reverse-order="true"
|
||||
go-to-container-logs="goToContainerLogs"
|
||||
></schedule-tasks-datatable>
|
||||
</uib-tab>
|
||||
|
||||
</uib-tabset>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
|
|
|
@ -1,12 +1,13 @@
|
|||
angular.module('portainer.app')
|
||||
.controller('ScheduleController', ['$q', '$scope', '$transition$', '$state', 'Notifications', 'EndpointService', 'GroupService', 'ScheduleService',
|
||||
function ($q, $scope, $transition$, $state, Notifications, EndpointService, GroupService, ScheduleService) {
|
||||
.controller('ScheduleController', ['$q', '$scope', '$transition$', '$state', 'Notifications', 'EndpointService', 'GroupService', 'ScheduleService', 'EndpointProvider',
|
||||
function ($q, $scope, $transition$, $state, Notifications, EndpointService, GroupService, ScheduleService, EndpointProvider) {
|
||||
|
||||
$scope.state = {
|
||||
actionInProgress: false
|
||||
};
|
||||
|
||||
$scope.update = update;
|
||||
$scope.goToContainerLogs = goToContainerLogs;
|
||||
|
||||
function update() {
|
||||
var model = $scope.schedule;
|
||||
|
@ -25,25 +26,48 @@ function ($q, $scope, $transition$, $state, Notifications, EndpointService, Grou
|
|||
});
|
||||
}
|
||||
|
||||
function goToContainerLogs(endpointId, containerId) {
|
||||
EndpointProvider.setEndpointID(endpointId);
|
||||
$state.go('docker.containers.container.logs', { id: containerId });
|
||||
}
|
||||
|
||||
function associateEndpointsToTasks(tasks, endpoints) {
|
||||
for (var i = 0; i < tasks.length; i++) {
|
||||
var task = tasks[i];
|
||||
|
||||
for (var j = 0; j < endpoints.length; j++) {
|
||||
var endpoint = endpoints[j];
|
||||
|
||||
if (task.EndpointId === endpoint.Id) {
|
||||
task.Endpoint = endpoint;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function initView() {
|
||||
var id = $transition$.params().id;
|
||||
var schedule = null;
|
||||
|
||||
$q.all({
|
||||
schedule: ScheduleService.schedule(id),
|
||||
file: ScheduleService.getScriptFile(id),
|
||||
tasks: ScheduleService.scriptExecutionTasks(id),
|
||||
endpoints: EndpointService.endpoints(),
|
||||
groups: GroupService.groups()
|
||||
})
|
||||
.then(function success(data) {
|
||||
schedule = data.schedule;
|
||||
var schedule = data.schedule;
|
||||
schedule.Job.FileContent = data.file.ScheduleFileContent;
|
||||
|
||||
var endpoints = data.endpoints;
|
||||
var tasks = data.tasks;
|
||||
associateEndpointsToTasks(tasks, endpoints);
|
||||
|
||||
$scope.schedule = schedule;
|
||||
$scope.tasks = data.tasks;
|
||||
$scope.endpoints = data.endpoints;
|
||||
$scope.groups = data.groups;
|
||||
|
||||
return ScheduleService.getScriptFile(schedule.Id);
|
||||
})
|
||||
.then(function success(data) {
|
||||
schedule.Job.FileContent = data.ScheduleFileContent;
|
||||
$scope.schedule = schedule;
|
||||
})
|
||||
.catch(function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to retrieve endpoint list');
|
||||
|
|
Loading…
Reference in New Issue