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)
|
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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -179,9 +179,9 @@ func loadEndpointSyncSystemSchedule(jobScheduler portainer.JobScheduler, schedul
|
||||||
}
|
}
|
||||||
|
|
||||||
endpointSyncJobContext := cron.NewEndpointSyncJobContext(endpointService, *flags.ExternalEndpoints)
|
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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -199,9 +199,9 @@ func loadSchedulesFromDatabase(jobScheduler portainer.JobScheduler, jobService p
|
||||||
|
|
||||||
if schedule.JobType == portainer.ScriptExecutionJobType {
|
if schedule.JobType == portainer.ScriptExecutionJobType {
|
||||||
jobContext := cron.NewScriptExecutionJobContext(jobService, endpointService, fileService)
|
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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,7 +11,7 @@ import (
|
||||||
|
|
||||||
// EndpointSyncJobRunner is used to run a EndpointSyncJob
|
// EndpointSyncJobRunner is used to run a EndpointSyncJob
|
||||||
type EndpointSyncJobRunner struct {
|
type EndpointSyncJobRunner struct {
|
||||||
job *portainer.EndpointSyncJob
|
schedule *portainer.Schedule
|
||||||
context *EndpointSyncJobContext
|
context *EndpointSyncJobContext
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -30,9 +30,9 @@ func NewEndpointSyncJobContext(endpointService portainer.EndpointService, endpoi
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewEndpointSyncJobRunner returns a new runner that can be scheduled
|
// 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{
|
return &EndpointSyncJobRunner{
|
||||||
job: job,
|
schedule: schedule,
|
||||||
context: context,
|
context: context,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -53,19 +53,9 @@ type fileEndpoint struct {
|
||||||
TLSKey string `json:"TLSKey,omitempty"`
|
TLSKey string `json:"TLSKey,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetScheduleID returns the schedule identifier associated to the runner
|
// GetSchedule returns the schedule associated to the runner
|
||||||
func (runner *EndpointSyncJobRunner) GetScheduleID() portainer.ScheduleID {
|
func (runner *EndpointSyncJobRunner) GetSchedule() *portainer.Schedule {
|
||||||
return runner.job.ScheduleID
|
return runner.schedule
|
||||||
}
|
|
||||||
|
|
||||||
// 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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Run triggers the execution of the endpoint synchronization process.
|
// Run triggers the execution of the endpoint synchronization process.
|
||||||
|
|
|
@ -9,7 +9,7 @@ import (
|
||||||
|
|
||||||
// ScriptExecutionJobRunner is used to run a ScriptExecutionJob
|
// ScriptExecutionJobRunner is used to run a ScriptExecutionJob
|
||||||
type ScriptExecutionJobRunner struct {
|
type ScriptExecutionJobRunner struct {
|
||||||
job *portainer.ScriptExecutionJob
|
schedule *portainer.Schedule
|
||||||
context *ScriptExecutionJobContext
|
context *ScriptExecutionJobContext
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -30,9 +30,9 @@ func NewScriptExecutionJobContext(jobService portainer.JobService, endpointServi
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewScriptExecutionJobRunner returns a new runner that can be scheduled
|
// 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{
|
return &ScriptExecutionJobRunner{
|
||||||
job: job,
|
schedule: schedule,
|
||||||
context: context,
|
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
|
// It will iterate through all the endpoints specified in the context to
|
||||||
// execute the script associated to the job.
|
// execute the script associated to the job.
|
||||||
func (runner *ScriptExecutionJobRunner) Run() {
|
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 {
|
if err != nil {
|
||||||
log.Printf("scheduled job error (script execution). Unable to retrieve script file (err=%s)\n", err)
|
log.Printf("scheduled job error (script execution). Unable to retrieve script file (err=%s)\n", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
targets := make([]*portainer.Endpoint, 0)
|
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)
|
endpoint, err := runner.context.endpointService.Endpoint(endpointID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("scheduled job error (script execution). Unable to retrieve information about endpoint (id=%d) (err=%s)\n", endpointID, err)
|
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)
|
retryTargets := make([]*portainer.Endpoint, 0)
|
||||||
|
|
||||||
for _, endpoint := range endpoints {
|
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 {
|
if err == portainer.ErrUnableToPingEndpoint {
|
||||||
retryTargets = append(retryTargets, endpoint)
|
retryTargets = append(retryTargets, endpoint)
|
||||||
} else if err != nil {
|
} else if err != nil {
|
||||||
|
@ -74,26 +74,16 @@ func (runner *ScriptExecutionJobRunner) executeAndRetry(endpoints []*portainer.E
|
||||||
}
|
}
|
||||||
|
|
||||||
retryCount++
|
retryCount++
|
||||||
if retryCount >= runner.job.RetryCount {
|
if retryCount >= runner.schedule.ScriptExecutionJob.RetryCount {
|
||||||
return
|
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)
|
runner.executeAndRetry(retryTargets, script, retryCount)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetScheduleID returns the schedule identifier associated to the runner
|
// GetSchedule returns the schedule associated to the runner
|
||||||
func (runner *ScriptExecutionJobRunner) GetScheduleID() portainer.ScheduleID {
|
func (runner *ScriptExecutionJobRunner) GetSchedule() *portainer.Schedule {
|
||||||
return runner.job.ScheduleID
|
return runner.schedule
|
||||||
}
|
|
||||||
|
|
||||||
// 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
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,7 +8,7 @@ import (
|
||||||
|
|
||||||
// SnapshotJobRunner is used to run a SnapshotJob
|
// SnapshotJobRunner is used to run a SnapshotJob
|
||||||
type SnapshotJobRunner struct {
|
type SnapshotJobRunner struct {
|
||||||
job *portainer.SnapshotJob
|
schedule *portainer.Schedule
|
||||||
context *SnapshotJobContext
|
context *SnapshotJobContext
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -27,35 +27,25 @@ func NewSnapshotJobContext(endpointService portainer.EndpointService, snapshotte
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewSnapshotJobRunner returns a new runner that can be scheduled
|
// 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{
|
return &SnapshotJobRunner{
|
||||||
job: job,
|
schedule: schedule,
|
||||||
context: context,
|
context: context,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetScheduleID returns the schedule identifier associated to the runner
|
// GetSchedule returns the schedule associated to the runner
|
||||||
func (runner *SnapshotJobRunner) GetScheduleID() portainer.ScheduleID {
|
func (runner *SnapshotJobRunner) GetSchedule() *portainer.Schedule {
|
||||||
return runner.job.ScheduleID
|
return runner.schedule
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetScheduleID sets the schedule identifier associated to the runner
|
// Run triggers the execution of the schedule.
|
||||||
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.
|
|
||||||
// It will iterate through all the endpoints available in the database to
|
// It will iterate through all the endpoints available in the database to
|
||||||
// create a snapshot of each one of them.
|
// create a snapshot of each one of them.
|
||||||
func (runner *SnapshotJobRunner) Run() {
|
func (runner *SnapshotJobRunner) Run() {
|
||||||
endpoints, err := runner.context.endpointService.Endpoints()
|
endpoints, err := runner.context.endpointService.Endpoints()
|
||||||
if err != nil {
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -67,7 +57,7 @@ func (runner *SnapshotJobRunner) Run() {
|
||||||
snapshot, err := runner.context.snapshotter.CreateSnapshot(&endpoint)
|
snapshot, err := runner.context.snapshotter.CreateSnapshot(&endpoint)
|
||||||
endpoint.Status = portainer.EndpointStatusUp
|
endpoint.Status = portainer.EndpointStatusUp
|
||||||
if err != nil {
|
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
|
endpoint.Status = portainer.EndpointStatusDown
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -77,7 +67,7 @@ func (runner *SnapshotJobRunner) Run() {
|
||||||
|
|
||||||
err = runner.context.endpointService.UpdateEndpoint(endpoint.ID, &endpoint)
|
err = runner.context.endpointService.UpdateEndpoint(endpoint.ID, &endpoint)
|
||||||
if err != nil {
|
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
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,31 +17,25 @@ func NewJobScheduler() *JobScheduler {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateSchedule schedules the execution of a job via a runner
|
// ScheduleJob schedules the execution of a job via a runner
|
||||||
func (scheduler *JobScheduler) CreateSchedule(schedule *portainer.Schedule, runner portainer.JobRunner) error {
|
func (scheduler *JobScheduler) ScheduleJob(runner portainer.JobRunner) error {
|
||||||
runner.SetScheduleID(schedule.ID)
|
return scheduler.cron.AddJob(runner.GetSchedule().CronExpression, runner)
|
||||||
return scheduler.cron.AddJob(schedule.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
|
// 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
|
// NOTE: the cron library do not support updating schedules directly
|
||||||
// hence the work-around
|
// 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()
|
cronEntries := scheduler.cron.Entries()
|
||||||
newCron := cron.New()
|
newCron := cron.New()
|
||||||
|
|
||||||
for _, entry := range cronEntries {
|
for _, entry := range cronEntries {
|
||||||
|
if entry.Job.(portainer.JobRunner).GetSchedule().JobType == jobType {
|
||||||
if entry.Job.(portainer.JobRunner).GetScheduleID() == schedule.ID {
|
err := newCron.AddJob(newCronExpression, entry.Job)
|
||||||
|
|
||||||
var jobRunner cron.Job = runner
|
|
||||||
if entry.Job.(portainer.JobRunner).GetJobType() == portainer.SnapshotJobType {
|
|
||||||
jobRunner = entry.Job
|
|
||||||
}
|
|
||||||
|
|
||||||
err := newCron.AddJob(schedule.CronExpression, jobRunner)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -56,17 +50,50 @@ func (scheduler *JobScheduler) UpdateSchedule(schedule *portainer.Schedule, runn
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// RemoveSchedule remove a scheduled job by re-creating a new cron
|
// UpdateJobSchedule updates a specific scheduled job by re-creating a new cron
|
||||||
// and adding all the existing jobs except for the one specified via scheduleID.
|
// and adding all the existing jobs. It will then re-schedule the new job
|
||||||
// NOTE: the cron library do not support removing schedules directly
|
// via the specified JobRunner parameter.
|
||||||
|
// NOTE: the cron library do not support updating schedules directly
|
||||||
// hence the work-around
|
// hence the work-around
|
||||||
func (scheduler *JobScheduler) RemoveSchedule(scheduleID portainer.ScheduleID) {
|
func (scheduler *JobScheduler) UpdateJobSchedule(runner portainer.JobRunner) error {
|
||||||
cronEntries := scheduler.cron.Entries()
|
cronEntries := scheduler.cron.Entries()
|
||||||
newCron := cron.New()
|
newCron := cron.New()
|
||||||
|
|
||||||
for _, entry := range cronEntries {
|
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
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -18,24 +18,25 @@ import (
|
||||||
|
|
||||||
// JobService represents a service that handles the execution of jobs
|
// JobService represents a service that handles the execution of jobs
|
||||||
type JobService struct {
|
type JobService struct {
|
||||||
DockerClientFactory *ClientFactory
|
dockerClientFactory *ClientFactory
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewJobService returns a pointer to a new job service
|
// NewJobService returns a pointer to a new job service
|
||||||
func NewJobService(dockerClientFactory *ClientFactory) *JobService {
|
func NewJobService(dockerClientFactory *ClientFactory) *JobService {
|
||||||
return &JobService{
|
return &JobService{
|
||||||
DockerClientFactory: dockerClientFactory,
|
dockerClientFactory: dockerClientFactory,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Execute will execute a script on the endpoint host with the supplied image as a container
|
// ExecuteScript will leverage a privileged container to execute a script against the specified endpoint/nodename.
|
||||||
func (service *JobService) Execute(endpoint *portainer.Endpoint, nodeName, image string, script []byte) error {
|
// 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)
|
buffer, err := archive.TarFileInBuffer(script, "script.sh", 0700)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
cli, err := service.DockerClientFactory.CreateClient(endpoint, nodeName)
|
cli, err := service.dockerClientFactory.CreateClient(endpoint, nodeName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -64,6 +65,10 @@ func (service *JobService) Execute(endpoint *portainer.Endpoint, nodeName, image
|
||||||
Cmd: strslice.StrSlice([]string{"sh", "/tmp/script.sh"}),
|
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{
|
hostConfig := &container.HostConfig{
|
||||||
Binds: []string{"/:/host", "/etc:/etc:ro", "/usr:/usr:ro", "/run:/run:ro", "/sbin:/sbin:ro", "/var:/var:ro"},
|
Binds: []string{"/:/host", "/etc:/etc:ro", "/usr:/usr:ro", "/run:/run:ro", "/sbin:/sbin:ro", "/var:/var:ro"},
|
||||||
NetworkMode: "host",
|
NetworkMode: "host",
|
||||||
|
@ -77,6 +82,13 @@ func (service *JobService) Execute(endpoint *portainer.Endpoint, nodeName, image
|
||||||
return err
|
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{}
|
copyOptions := types.CopyToContainerOptions{}
|
||||||
err = cli.CopyToContainer(context.Background(), body.ID, "/tmp", bytes.NewReader(buffer), copyOptions)
|
err = cli.CopyToContainer(context.Background(), body.ID, "/tmp", bytes.NewReader(buffer), copyOptions)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -84,12 +96,7 @@ func (service *JobService) Execute(endpoint *portainer.Endpoint, nodeName, image
|
||||||
}
|
}
|
||||||
|
|
||||||
startOptions := types.ContainerStartOptions{}
|
startOptions := types.ContainerStartOptions{}
|
||||||
err = cli.ContainerStart(context.Background(), body.ID, startOptions)
|
return cli.ContainerStart(context.Background(), body.ID, startOptions)
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func pullImage(cli *client.Client, image string) error {
|
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}
|
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 {
|
if err != nil {
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Failed executing job", err}
|
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}
|
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 {
|
if err != nil {
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Failed executing job", err}
|
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)
|
bouncer.AdministratorAccess(httperror.LoggerHandler(h.scheduleDelete))).Methods(http.MethodDelete)
|
||||||
h.Handle("/schedules/{id}/file",
|
h.Handle("/schedules/{id}/file",
|
||||||
bouncer.AdministratorAccess(httperror.LoggerHandler(h.scheduleFile))).Methods(http.MethodGet)
|
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
|
return h
|
||||||
}
|
}
|
||||||
|
|
|
@ -160,7 +160,7 @@ func (handler *Handler) createScheduleObjectFromFilePayload(payload *scheduleCre
|
||||||
job := &portainer.ScriptExecutionJob{
|
job := &portainer.ScriptExecutionJob{
|
||||||
Endpoints: payload.Endpoints,
|
Endpoints: payload.Endpoints,
|
||||||
Image: payload.Image,
|
Image: payload.Image,
|
||||||
ScheduleID: scheduleIdentifier,
|
// ScheduleID: scheduleIdentifier,
|
||||||
RetryCount: payload.RetryCount,
|
RetryCount: payload.RetryCount,
|
||||||
RetryInterval: payload.RetryInterval,
|
RetryInterval: payload.RetryInterval,
|
||||||
}
|
}
|
||||||
|
@ -183,7 +183,7 @@ func (handler *Handler) createScheduleObjectFromFileContentPayload(payload *sche
|
||||||
job := &portainer.ScriptExecutionJob{
|
job := &portainer.ScriptExecutionJob{
|
||||||
Endpoints: payload.Endpoints,
|
Endpoints: payload.Endpoints,
|
||||||
Image: payload.Image,
|
Image: payload.Image,
|
||||||
ScheduleID: scheduleIdentifier,
|
// ScheduleID: scheduleIdentifier,
|
||||||
RetryCount: payload.RetryCount,
|
RetryCount: payload.RetryCount,
|
||||||
RetryInterval: payload.RetryInterval,
|
RetryInterval: payload.RetryInterval,
|
||||||
}
|
}
|
||||||
|
@ -209,9 +209,9 @@ func (handler *Handler) addAndPersistSchedule(schedule *portainer.Schedule, file
|
||||||
schedule.ScriptExecutionJob.ScriptPath = scriptPath
|
schedule.ScriptExecutionJob.ScriptPath = scriptPath
|
||||||
|
|
||||||
jobContext := cron.NewScriptExecutionJobContext(handler.JobService, handler.EndpointService, handler.FileService)
|
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 {
|
if err != nil {
|
||||||
return err
|
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}
|
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))
|
err = handler.ScheduleService.DeleteSchedule(portainer.ScheduleID(scheduleID))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to remove the schedule from the database", err}
|
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 {
|
if updateJobSchedule {
|
||||||
jobContext := cron.NewScriptExecutionJobContext(handler.JobService, handler.EndpointService, handler.FileService)
|
jobContext := cron.NewScriptExecutionJobContext(handler.JobService, handler.EndpointService, handler.FileService)
|
||||||
jobRunner := cron.NewScriptExecutionJobRunner(schedule.ScriptExecutionJob, jobContext)
|
jobRunner := cron.NewScriptExecutionJobRunner(schedule, jobContext)
|
||||||
err := handler.JobScheduler.UpdateSchedule(schedule, jobRunner)
|
err := handler.JobScheduler.UpdateJobSchedule(jobRunner)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to update job scheduler", err}
|
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 := schedules[0]
|
||||||
snapshotSchedule.CronExpression = "@every " + snapshotInterval
|
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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
@ -228,7 +228,6 @@ type (
|
||||||
|
|
||||||
// ScriptExecutionJob represents a scheduled job that can execute a script via a privileged container
|
// ScriptExecutionJob represents a scheduled job that can execute a script via a privileged container
|
||||||
ScriptExecutionJob struct {
|
ScriptExecutionJob struct {
|
||||||
ScheduleID ScheduleID `json:"ScheduleId"`
|
|
||||||
Endpoints []EndpointID
|
Endpoints []EndpointID
|
||||||
Image string
|
Image string
|
||||||
ScriptPath string
|
ScriptPath string
|
||||||
|
@ -237,14 +236,10 @@ type (
|
||||||
}
|
}
|
||||||
|
|
||||||
// SnapshotJob represents a scheduled job that can create endpoint snapshots
|
// SnapshotJob represents a scheduled job that can create endpoint snapshots
|
||||||
SnapshotJob struct {
|
SnapshotJob struct{}
|
||||||
ScheduleID ScheduleID `json:"ScheduleId"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// EndpointSyncJob represents a scheduled job that synchronize endpoints based on an external file
|
// EndpointSyncJob represents a scheduled job that synchronize endpoints based on an external file
|
||||||
EndpointSyncJob struct {
|
EndpointSyncJob struct{}
|
||||||
ScheduleID ScheduleID `json:"ScheduleId"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Schedule represents a scheduled job.
|
// Schedule represents a scheduled job.
|
||||||
// It only contains a pointer to one of the JobRunner implementations
|
// 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 represents a service to run jobs on a periodic basis
|
||||||
JobScheduler interface {
|
JobScheduler interface {
|
||||||
CreateSchedule(schedule *Schedule, runner JobRunner) error
|
ScheduleJob(runner JobRunner) error
|
||||||
UpdateSchedule(schedule *Schedule, runner JobRunner) error
|
UpdateJobSchedule(runner JobRunner) error
|
||||||
RemoveSchedule(ID ScheduleID)
|
UpdateSystemJobSchedule(jobType JobType, newCronExpression string) error
|
||||||
|
UnscheduleJob(ID ScheduleID)
|
||||||
Start()
|
Start()
|
||||||
}
|
}
|
||||||
|
|
||||||
// JobRunner represents a service that can be used to run a job
|
// JobRunner represents a service that can be used to run a job
|
||||||
JobRunner interface {
|
JobRunner interface {
|
||||||
Run()
|
Run()
|
||||||
GetScheduleID() ScheduleID
|
GetSchedule() *Schedule
|
||||||
SetScheduleID(ID ScheduleID)
|
|
||||||
GetJobType() JobType
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Snapshotter represents a service used to create endpoint snapshots
|
// 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 represents a service to manage job execution on hosts
|
||||||
JobService interface {
|
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))">
|
<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>
|
<td>
|
||||||
<a ui-sref="docker.containers.container.logs({ id: item.Id, nodeName: item.NodeName })" title="{{ item.Id }}">
|
<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>
|
||||||
<td>
|
<td>
|
||||||
<span ng-if="['starting','healthy','unhealthy'].indexOf(item.Status) !== -1" class="label label-{{ item.Status|containerstatusbadge }} interactive"
|
<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;
|
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) {
|
function ScheduleCreateRequest(model) {
|
||||||
this.Name = model.Name;
|
this.Name = model.Name;
|
||||||
this.CronExpression = model.CronExpression;
|
this.CronExpression = model.CronExpression;
|
||||||
|
|
|
@ -8,6 +8,7 @@ function SchedulesFactory($resource, API_ENDPOINT_SCHEDULES) {
|
||||||
get: { method: 'GET', params: { id: '@id' } },
|
get: { method: 'GET', params: { id: '@id' } },
|
||||||
update: { method: 'PUT', params: { id: '@id' } },
|
update: { method: 'PUT', params: { id: '@id' } },
|
||||||
remove: { method: 'DELETE', params: { id: '@id'} },
|
remove: { method: 'DELETE', params: { id: '@id'} },
|
||||||
file: { method: 'GET', params: { id : '@id', action: 'file' } }
|
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;
|
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) {
|
service.createScheduleFromFileContent = function(model) {
|
||||||
var payload = new ScheduleCreateRequest(model);
|
var payload = new ScheduleCreateRequest(model);
|
||||||
return Schedules.create({ method: 'string' }, payload).$promise;
|
return Schedules.create({ method: 'string' }, payload).$promise;
|
||||||
|
|
|
@ -13,6 +13,13 @@
|
||||||
<div class="col-sm-12">
|
<div class="col-sm-12">
|
||||||
<rd-widget>
|
<rd-widget>
|
||||||
<rd-widget-body>
|
<rd-widget-body>
|
||||||
|
|
||||||
|
<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
|
<schedule-form
|
||||||
ng-if="schedule"
|
ng-if="schedule"
|
||||||
model="schedule"
|
model="schedule"
|
||||||
|
@ -22,6 +29,37 @@
|
||||||
form-action-label="Update schedule"
|
form-action-label="Update schedule"
|
||||||
action-in-progress="state.actionInProgress"
|
action-in-progress="state.actionInProgress"
|
||||||
></schedule-form>
|
></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-body>
|
||||||
</rd-widget>
|
</rd-widget>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,12 +1,13 @@
|
||||||
angular.module('portainer.app')
|
angular.module('portainer.app')
|
||||||
.controller('ScheduleController', ['$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) {
|
function ($q, $scope, $transition$, $state, Notifications, EndpointService, GroupService, ScheduleService, EndpointProvider) {
|
||||||
|
|
||||||
$scope.state = {
|
$scope.state = {
|
||||||
actionInProgress: false
|
actionInProgress: false
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.update = update;
|
$scope.update = update;
|
||||||
|
$scope.goToContainerLogs = goToContainerLogs;
|
||||||
|
|
||||||
function update() {
|
function update() {
|
||||||
var model = $scope.schedule;
|
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() {
|
function initView() {
|
||||||
var id = $transition$.params().id;
|
var id = $transition$.params().id;
|
||||||
var schedule = null;
|
|
||||||
|
|
||||||
$q.all({
|
$q.all({
|
||||||
schedule: ScheduleService.schedule(id),
|
schedule: ScheduleService.schedule(id),
|
||||||
|
file: ScheduleService.getScriptFile(id),
|
||||||
|
tasks: ScheduleService.scriptExecutionTasks(id),
|
||||||
endpoints: EndpointService.endpoints(),
|
endpoints: EndpointService.endpoints(),
|
||||||
groups: GroupService.groups()
|
groups: GroupService.groups()
|
||||||
})
|
})
|
||||||
.then(function success(data) {
|
.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.endpoints = data.endpoints;
|
||||||
$scope.groups = data.groups;
|
$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) {
|
.catch(function error(err) {
|
||||||
Notifications.error('Failure', err, 'Unable to retrieve endpoint list');
|
Notifications.error('Failure', err, 'Unable to retrieve endpoint list');
|
||||||
|
|
Loading…
Reference in New Issue