mirror of https://github.com/portainer/portainer
Merge branch 'develop' into webpack
commit
3c66fa8084
|
@ -2,6 +2,7 @@ package main // import "github.com/portainer/portainer"
|
|||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
|
@ -88,7 +89,7 @@ func initJWTService(authenticationEnabled bool) portainer.JWTService {
|
|||
}
|
||||
|
||||
func initDigitalSignatureService() portainer.DigitalSignatureService {
|
||||
return &crypto.ECDSAService{}
|
||||
return crypto.NewECDSAService(os.Getenv("AGENT_SECRET"))
|
||||
}
|
||||
|
||||
func initCryptoService() portainer.CryptoService {
|
||||
|
@ -141,9 +142,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 +180,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 +200,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.
|
||||
|
|
|
@ -2,14 +2,15 @@ package cron
|
|||
|
||||
import (
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"github.com/portainer/portainer"
|
||||
)
|
||||
|
||||
// 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
|
||||
|
@ -29,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,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -40,37 +41,49 @@ 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
|
||||
}
|
||||
|
||||
for _, endpointID := range runner.job.Endpoints {
|
||||
targets := make([]*portainer.Endpoint, 0)
|
||||
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)
|
||||
return
|
||||
}
|
||||
|
||||
err = runner.context.jobService.Execute(endpoint, "", runner.job.Image, scriptFile)
|
||||
if err != nil {
|
||||
log.Printf("scheduled job error (script execution). Unable to execute scrtip (endpoint=%s) (err=%s)\n", endpoint.Name, err)
|
||||
targets = append(targets, endpoint)
|
||||
}
|
||||
|
||||
runner.executeAndRetry(targets, scriptFile, 0)
|
||||
}
|
||||
|
||||
func (runner *ScriptExecutionJobRunner) executeAndRetry(endpoints []*portainer.Endpoint, script []byte, retryCount int) {
|
||||
retryTargets := make([]*portainer.Endpoint, 0)
|
||||
|
||||
for _, endpoint := range endpoints {
|
||||
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 {
|
||||
log.Printf("scheduled job error (script execution). Unable to execute script (endpoint=%s) (err=%s)\n", endpoint.Name, err)
|
||||
}
|
||||
}
|
||||
|
||||
retryCount++
|
||||
if retryCount >= runner.schedule.ScriptExecutionJob.RetryCount {
|
||||
return
|
||||
}
|
||||
|
||||
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,27 @@ 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.
|
||||
// As a snapshot can be a long process, to avoid any concurrency issue we
|
||||
// retrieve the latest version of the endpoint right after a snapshot.
|
||||
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
|
||||
}
|
||||
|
||||
|
@ -64,20 +56,27 @@ func (runner *SnapshotJobRunner) Run() {
|
|||
continue
|
||||
}
|
||||
|
||||
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)
|
||||
endpoint.Status = portainer.EndpointStatusDown
|
||||
snapshot, snapshotError := runner.context.snapshotter.CreateSnapshot(&endpoint)
|
||||
|
||||
latestEndpointReference, err := runner.context.endpointService.Endpoint(endpoint.ID)
|
||||
if latestEndpointReference == nil {
|
||||
log.Printf("background schedule error (endpoint snapshot). Endpoint not found inside the database anymore (endpoint=%s, URL=%s) (err=%s)\n", endpoint.Name, endpoint.URL, err)
|
||||
continue
|
||||
}
|
||||
|
||||
latestEndpointReference.Status = portainer.EndpointStatusUp
|
||||
if snapshotError != nil {
|
||||
log.Printf("background schedule error (endpoint snapshot). Unable to create snapshot (endpoint=%s, URL=%s) (err=%s)\n", endpoint.Name, endpoint.URL, snapshotError)
|
||||
latestEndpointReference.Status = portainer.EndpointStatusDown
|
||||
}
|
||||
|
||||
if snapshot != nil {
|
||||
endpoint.Snapshots = []portainer.Snapshot{*snapshot}
|
||||
latestEndpointReference.Snapshots = []portainer.Snapshot{*snapshot}
|
||||
}
|
||||
|
||||
err = runner.context.endpointService.UpdateEndpoint(endpoint.ID, &endpoint)
|
||||
err = runner.context.endpointService.UpdateEndpoint(latestEndpointReference.ID, latestEndpointReference)
|
||||
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
|
||||
}
|
||||
|
||||
|
|
|
@ -8,6 +8,8 @@ import (
|
|||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"math/big"
|
||||
|
||||
"github.com/portainer/portainer"
|
||||
)
|
||||
|
||||
const (
|
||||
|
@ -26,6 +28,15 @@ type ECDSAService struct {
|
|||
privateKey *ecdsa.PrivateKey
|
||||
publicKey *ecdsa.PublicKey
|
||||
encodedPubKey string
|
||||
secret string
|
||||
}
|
||||
|
||||
// NewECDSAService returns a pointer to a ECDSAService.
|
||||
// An optional secret can be specified
|
||||
func NewECDSAService(secret string) *ECDSAService {
|
||||
return &ECDSAService{
|
||||
secret: secret,
|
||||
}
|
||||
}
|
||||
|
||||
// EncodedPublicKey returns the encoded version of the public that can be used
|
||||
|
@ -91,11 +102,17 @@ func (service *ECDSAService) GenerateKeyPair() ([]byte, []byte, error) {
|
|||
return private, public, nil
|
||||
}
|
||||
|
||||
// Sign creates a signature from a message.
|
||||
// It automatically hash the message using MD5 and creates a signature from
|
||||
// CreateSignature creates a digital signature.
|
||||
// It automatically hash a specific message using MD5 and creates a signature from
|
||||
// that hash.
|
||||
// It then encodes the generated signature in base64.
|
||||
func (service *ECDSAService) Sign(message string) (string, error) {
|
||||
func (service *ECDSAService) CreateSignature() (string, error) {
|
||||
|
||||
message := portainer.PortainerAgentSignatureMessage
|
||||
if service.secret != "" {
|
||||
message = service.secret
|
||||
}
|
||||
|
||||
hash := HashFromBytes([]byte(message))
|
||||
|
||||
r := big.NewInt(0)
|
||||
|
|
|
@ -67,7 +67,7 @@ func createAgentClient(endpoint *portainer.Endpoint, signatureService portainer.
|
|||
return nil, err
|
||||
}
|
||||
|
||||
signature, err := signatureService.Sign(portainer.PortainerAgentSignatureMessage)
|
||||
signature, err := signatureService.CreateSignature()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
|
@ -18,29 +18,35 @@ 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
|
||||
}
|
||||
defer cli.Close()
|
||||
|
||||
_, err = cli.Ping(context.Background())
|
||||
if err != nil {
|
||||
return portainer.ErrUnableToPingEndpoint
|
||||
}
|
||||
|
||||
err = pullImage(cli, image)
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -59,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",
|
||||
|
@ -72,6 +82,13 @@ func (service *JobService) Execute(endpoint *portainer.Endpoint, nodeName, image
|
|||
return err
|
||||
}
|
||||
|
||||
if schedule != nil {
|
||||
err = cli.ContainerRename(context.Background(), body.ID, 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 {
|
||||
|
@ -79,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 {
|
||||
|
|
|
@ -88,6 +88,11 @@ const (
|
|||
ErrUndefinedTLSFileType = Error("Undefined TLS file type")
|
||||
)
|
||||
|
||||
// Docker errors.
|
||||
const (
|
||||
ErrUnableToPingEndpoint = Error("Unable to communicate with the endpoint")
|
||||
)
|
||||
|
||||
// Error represents an application error.
|
||||
type Error string
|
||||
|
||||
|
|
|
@ -140,7 +140,7 @@ func (manager *SwarmStackManager) updateDockerCLIConfiguration(dataPath string)
|
|||
return err
|
||||
}
|
||||
|
||||
signature, err := manager.signatureService.Sign(portainer.PortainerAgentSignatureMessage)
|
||||
signature, err := manager.signatureService.CreateSignature()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package endpointproxy
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strconv"
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
|
@ -23,6 +24,10 @@ func (handler *Handler) proxyRequestsToDockerAPI(w http.ResponseWriter, r *http.
|
|||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err}
|
||||
}
|
||||
|
||||
if endpoint.Status == portainer.EndpointStatusDown {
|
||||
return &httperror.HandlerError{http.StatusServiceUnavailable, "Unable to query endpoint", errors.New("Endpoint is down")}
|
||||
}
|
||||
|
||||
err = handler.requestBouncer.EndpointAccess(r, endpoint)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", portainer.ErrEndpointAccessDenied}
|
||||
|
|
|
@ -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}
|
||||
}
|
||||
|
|
|
@ -1,41 +1,51 @@
|
|||
package endpoints
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
"github.com/portainer/libhttp/response"
|
||||
"github.com/portainer/portainer"
|
||||
)
|
||||
|
||||
// POST request on /api/endpoints/snapshot
|
||||
// POST request on /api/endpoints/:id/snapshot
|
||||
func (handler *Handler) endpointSnapshot(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
endpoints, err := handler.EndpointService.Endpoints()
|
||||
endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve endpoints from the database", err}
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid endpoint identifier route variable", err}
|
||||
}
|
||||
|
||||
for _, endpoint := range endpoints {
|
||||
if endpoint.Type == portainer.AzureEnvironment {
|
||||
continue
|
||||
}
|
||||
endpoint, err := handler.EndpointService.Endpoint(portainer.EndpointID(endpointID))
|
||||
if err == portainer.ErrObjectNotFound {
|
||||
return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint with the specified identifier inside the database", err}
|
||||
} else if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err}
|
||||
}
|
||||
|
||||
snapshot, err := handler.Snapshotter.CreateSnapshot(&endpoint)
|
||||
endpoint.Status = portainer.EndpointStatusUp
|
||||
if err != nil {
|
||||
log.Printf("http error: endpoint snapshot error (endpoint=%s, URL=%s) (err=%s)\n", endpoint.Name, endpoint.URL, err)
|
||||
endpoint.Status = portainer.EndpointStatusDown
|
||||
}
|
||||
if endpoint.Type == portainer.AzureEnvironment {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Snapshots not supported for Azure endpoints", err}
|
||||
}
|
||||
|
||||
if snapshot != nil {
|
||||
endpoint.Snapshots = []portainer.Snapshot{*snapshot}
|
||||
}
|
||||
snapshot, snapshotError := handler.Snapshotter.CreateSnapshot(endpoint)
|
||||
|
||||
err = handler.EndpointService.UpdateEndpoint(endpoint.ID, &endpoint)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist endpoint changes inside the database", err}
|
||||
}
|
||||
latestEndpointReference, err := handler.EndpointService.Endpoint(endpoint.ID)
|
||||
if latestEndpointReference == nil {
|
||||
return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint with the specified identifier inside the database", err}
|
||||
}
|
||||
|
||||
latestEndpointReference.Status = portainer.EndpointStatusUp
|
||||
if snapshotError != nil {
|
||||
latestEndpointReference.Status = portainer.EndpointStatusDown
|
||||
}
|
||||
|
||||
if snapshot != nil {
|
||||
latestEndpointReference.Snapshots = []portainer.Snapshot{*snapshot}
|
||||
}
|
||||
|
||||
err = handler.EndpointService.UpdateEndpoint(latestEndpointReference.ID, latestEndpointReference)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist endpoint changes inside the database", err}
|
||||
}
|
||||
|
||||
return response.Empty(w)
|
||||
|
|
|
@ -0,0 +1,49 @@
|
|||
package endpoints
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/response"
|
||||
"github.com/portainer/portainer"
|
||||
)
|
||||
|
||||
// POST request on /api/endpoints/snapshot
|
||||
func (handler *Handler) endpointSnapshots(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
endpoints, err := handler.EndpointService.Endpoints()
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve endpoints from the database", err}
|
||||
}
|
||||
|
||||
for _, endpoint := range endpoints {
|
||||
if endpoint.Type == portainer.AzureEnvironment {
|
||||
continue
|
||||
}
|
||||
|
||||
snapshot, snapshotError := handler.Snapshotter.CreateSnapshot(&endpoint)
|
||||
|
||||
latestEndpointReference, err := handler.EndpointService.Endpoint(endpoint.ID)
|
||||
if latestEndpointReference == nil {
|
||||
log.Printf("background schedule error (endpoint snapshot). Endpoint not found inside the database anymore (endpoint=%s, URL=%s) (err=%s)\n", endpoint.Name, endpoint.URL, err)
|
||||
continue
|
||||
}
|
||||
|
||||
latestEndpointReference.Status = portainer.EndpointStatusUp
|
||||
if snapshotError != nil {
|
||||
log.Printf("background schedule error (endpoint snapshot). Unable to create snapshot (endpoint=%s, URL=%s) (err=%s)\n", endpoint.Name, endpoint.URL, snapshotError)
|
||||
latestEndpointReference.Status = portainer.EndpointStatusDown
|
||||
}
|
||||
|
||||
if snapshot != nil {
|
||||
latestEndpointReference.Snapshots = []portainer.Snapshot{*snapshot}
|
||||
}
|
||||
|
||||
err = handler.EndpointService.UpdateEndpoint(latestEndpointReference.ID, latestEndpointReference)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist endpoint changes inside the database", err}
|
||||
}
|
||||
}
|
||||
|
||||
return response.Empty(w)
|
||||
}
|
|
@ -45,7 +45,7 @@ func NewHandler(bouncer *security.RequestBouncer, authorizeEndpointManagement bo
|
|||
h.Handle("/endpoints",
|
||||
bouncer.AdministratorAccess(httperror.LoggerHandler(h.endpointCreate))).Methods(http.MethodPost)
|
||||
h.Handle("/endpoints/snapshot",
|
||||
bouncer.AdministratorAccess(httperror.LoggerHandler(h.endpointSnapshot))).Methods(http.MethodPost)
|
||||
bouncer.AdministratorAccess(httperror.LoggerHandler(h.endpointSnapshots))).Methods(http.MethodPost)
|
||||
h.Handle("/endpoints",
|
||||
bouncer.RestrictedAccess(httperror.LoggerHandler(h.endpointList))).Methods(http.MethodGet)
|
||||
h.Handle("/endpoints/{id}",
|
||||
|
@ -62,5 +62,7 @@ func NewHandler(bouncer *security.RequestBouncer, authorizeEndpointManagement bo
|
|||
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.endpointExtensionRemove))).Methods(http.MethodDelete)
|
||||
h.Handle("/endpoints/{id}/job",
|
||||
bouncer.AdministratorAccess(httperror.LoggerHandler(h.endpointJob))).Methods(http.MethodPost)
|
||||
h.Handle("/endpoints/{id}/snapshot",
|
||||
bouncer.AdministratorAccess(httperror.LoggerHandler(h.endpointSnapshot))).Methods(http.MethodPost)
|
||||
return h
|
||||
}
|
||||
|
|
|
@ -18,7 +18,7 @@ func (handler *Handler) motd(w http.ResponseWriter, r *http.Request) {
|
|||
|
||||
motd, err := client.Get(portainer.MessageOfTheDayURL, 0)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
response.JSON(w, &motdResponse{Message: ""})
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -14,32 +14,40 @@ import (
|
|||
"github.com/portainer/portainer/cron"
|
||||
)
|
||||
|
||||
type scheduleFromFilePayload struct {
|
||||
type scheduleCreateFromFilePayload struct {
|
||||
Name string
|
||||
Image string
|
||||
CronExpression string
|
||||
Endpoints []portainer.EndpointID
|
||||
File []byte
|
||||
RetryCount int
|
||||
RetryInterval int
|
||||
}
|
||||
|
||||
type scheduleFromFileContentPayload struct {
|
||||
type scheduleCreateFromFileContentPayload struct {
|
||||
Name string
|
||||
CronExpression string
|
||||
Image string
|
||||
Endpoints []portainer.EndpointID
|
||||
FileContent string
|
||||
RetryCount int
|
||||
RetryInterval int
|
||||
}
|
||||
|
||||
func (payload *scheduleFromFilePayload) Validate(r *http.Request) error {
|
||||
func (payload *scheduleCreateFromFilePayload) Validate(r *http.Request) error {
|
||||
name, err := request.RetrieveMultiPartFormValue(r, "Name", false)
|
||||
if err != nil {
|
||||
return errors.New("Invalid name")
|
||||
return errors.New("Invalid schedule name")
|
||||
}
|
||||
|
||||
if !govalidator.Matches(name, `^[a-zA-Z0-9][a-zA-Z0-9_.-]+$`) {
|
||||
return errors.New("Invalid schedule name format. Allowed characters are: [a-zA-Z0-9_.-]")
|
||||
}
|
||||
payload.Name = name
|
||||
|
||||
image, err := request.RetrieveMultiPartFormValue(r, "Image", false)
|
||||
if err != nil {
|
||||
return errors.New("Invalid image")
|
||||
return errors.New("Invalid schedule image")
|
||||
}
|
||||
payload.Image = image
|
||||
|
||||
|
@ -62,14 +70,24 @@ func (payload *scheduleFromFilePayload) Validate(r *http.Request) error {
|
|||
}
|
||||
payload.File = file
|
||||
|
||||
retryCount, _ := request.RetrieveNumericMultiPartFormValue(r, "RetryCount", true)
|
||||
payload.RetryCount = retryCount
|
||||
|
||||
retryInterval, _ := request.RetrieveNumericMultiPartFormValue(r, "RetryInterval", true)
|
||||
payload.RetryInterval = retryInterval
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (payload *scheduleFromFileContentPayload) Validate(r *http.Request) error {
|
||||
func (payload *scheduleCreateFromFileContentPayload) Validate(r *http.Request) error {
|
||||
if govalidator.IsNull(payload.Name) {
|
||||
return portainer.Error("Invalid schedule name")
|
||||
}
|
||||
|
||||
if !govalidator.Matches(payload.Name, `^[a-zA-Z0-9][a-zA-Z0-9_.-]+$`) {
|
||||
return errors.New("Invalid schedule name format. Allowed characters are: [a-zA-Z0-9_.-]")
|
||||
}
|
||||
|
||||
if govalidator.IsNull(payload.Image) {
|
||||
return portainer.Error("Invalid schedule image")
|
||||
}
|
||||
|
@ -86,6 +104,10 @@ func (payload *scheduleFromFileContentPayload) Validate(r *http.Request) error {
|
|||
return portainer.Error("Invalid script file content")
|
||||
}
|
||||
|
||||
if payload.RetryCount != 0 && payload.RetryInterval == 0 {
|
||||
return portainer.Error("RetryInterval must be set")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -107,71 +129,100 @@ func (handler *Handler) scheduleCreate(w http.ResponseWriter, r *http.Request) *
|
|||
}
|
||||
|
||||
func (handler *Handler) createScheduleFromFileContent(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
var payload scheduleFromFileContentPayload
|
||||
var payload scheduleCreateFromFileContentPayload
|
||||
err := request.DecodeAndValidateJSONPayload(r, &payload)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err}
|
||||
}
|
||||
|
||||
schedule, err := handler.createSchedule(payload.Name, payload.Image, payload.CronExpression, payload.Endpoints, []byte(payload.FileContent))
|
||||
schedule := handler.createScheduleObjectFromFileContentPayload(&payload)
|
||||
|
||||
err = handler.addAndPersistSchedule(schedule, []byte(payload.FileContent))
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Failed executing job", err}
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to schedule script job", err}
|
||||
}
|
||||
|
||||
return response.JSON(w, schedule)
|
||||
}
|
||||
|
||||
func (handler *Handler) createScheduleFromFile(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
payload := &scheduleFromFilePayload{}
|
||||
payload := &scheduleCreateFromFilePayload{}
|
||||
err := payload.Validate(r)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err}
|
||||
}
|
||||
|
||||
schedule, err := handler.createSchedule(payload.Name, payload.Image, payload.CronExpression, payload.Endpoints, payload.File)
|
||||
schedule := handler.createScheduleObjectFromFilePayload(payload)
|
||||
|
||||
err = handler.addAndPersistSchedule(schedule, payload.File)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Failed executing job", err}
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to schedule script job", err}
|
||||
}
|
||||
|
||||
return response.JSON(w, schedule)
|
||||
}
|
||||
|
||||
func (handler *Handler) createSchedule(name, image, cronExpression string, endpoints []portainer.EndpointID, file []byte) (*portainer.Schedule, error) {
|
||||
func (handler *Handler) createScheduleObjectFromFilePayload(payload *scheduleCreateFromFilePayload) *portainer.Schedule {
|
||||
scheduleIdentifier := portainer.ScheduleID(handler.ScheduleService.GetNextIdentifier())
|
||||
|
||||
scriptPath, err := handler.FileService.StoreScheduledJobFileFromBytes(strconv.Itoa(int(scheduleIdentifier)), file)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
job := &portainer.ScriptExecutionJob{
|
||||
Endpoints: endpoints,
|
||||
Image: image,
|
||||
ScriptPath: scriptPath,
|
||||
ScheduleID: scheduleIdentifier,
|
||||
Endpoints: payload.Endpoints,
|
||||
Image: payload.Image,
|
||||
// ScheduleID: scheduleIdentifier,
|
||||
RetryCount: payload.RetryCount,
|
||||
RetryInterval: payload.RetryInterval,
|
||||
}
|
||||
|
||||
schedule := &portainer.Schedule{
|
||||
ID: scheduleIdentifier,
|
||||
Name: name,
|
||||
CronExpression: cronExpression,
|
||||
Name: payload.Name,
|
||||
CronExpression: payload.CronExpression,
|
||||
JobType: portainer.ScriptExecutionJobType,
|
||||
ScriptExecutionJob: job,
|
||||
Created: time.Now().Unix(),
|
||||
}
|
||||
|
||||
jobContext := cron.NewScriptExecutionJobContext(handler.JobService, handler.EndpointService, handler.FileService)
|
||||
jobRunner := cron.NewScriptExecutionJobRunner(job, jobContext)
|
||||
|
||||
err = handler.JobScheduler.CreateSchedule(schedule, jobRunner)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = handler.ScheduleService.CreateSchedule(schedule)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return schedule, nil
|
||||
return schedule
|
||||
}
|
||||
|
||||
func (handler *Handler) createScheduleObjectFromFileContentPayload(payload *scheduleCreateFromFileContentPayload) *portainer.Schedule {
|
||||
scheduleIdentifier := portainer.ScheduleID(handler.ScheduleService.GetNextIdentifier())
|
||||
|
||||
job := &portainer.ScriptExecutionJob{
|
||||
Endpoints: payload.Endpoints,
|
||||
Image: payload.Image,
|
||||
// ScheduleID: scheduleIdentifier,
|
||||
RetryCount: payload.RetryCount,
|
||||
RetryInterval: payload.RetryInterval,
|
||||
}
|
||||
|
||||
schedule := &portainer.Schedule{
|
||||
ID: scheduleIdentifier,
|
||||
Name: payload.Name,
|
||||
CronExpression: payload.CronExpression,
|
||||
JobType: portainer.ScriptExecutionJobType,
|
||||
ScriptExecutionJob: job,
|
||||
Created: time.Now().Unix(),
|
||||
}
|
||||
|
||||
return schedule
|
||||
}
|
||||
|
||||
func (handler *Handler) addAndPersistSchedule(schedule *portainer.Schedule, file []byte) error {
|
||||
scriptPath, err := handler.FileService.StoreScheduledJobFileFromBytes(strconv.Itoa(int(schedule.ID)), file)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
schedule.ScriptExecutionJob.ScriptPath = scriptPath
|
||||
|
||||
jobContext := cron.NewScriptExecutionJobContext(handler.JobService, handler.EndpointService, handler.FileService)
|
||||
jobRunner := cron.NewScriptExecutionJobRunner(schedule, jobContext)
|
||||
|
||||
err = handler.JobScheduler.ScheduleJob(jobRunner)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return handler.ScheduleService.CreateSchedule(schedule)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -1,9 +1,11 @@
|
|||
package schedules
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/asaskevich/govalidator"
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
"github.com/portainer/libhttp/response"
|
||||
|
@ -17,9 +19,14 @@ type scheduleUpdatePayload struct {
|
|||
CronExpression *string
|
||||
Endpoints []portainer.EndpointID
|
||||
FileContent *string
|
||||
RetryCount *int
|
||||
RetryInterval *int
|
||||
}
|
||||
|
||||
func (payload *scheduleUpdatePayload) Validate(r *http.Request) error {
|
||||
if payload.Name != nil && !govalidator.Matches(*payload.Name, `^[a-zA-Z0-9][a-zA-Z0-9_.-]+$`) {
|
||||
return errors.New("Invalid schedule name format. Allowed characters are: [a-zA-Z0-9_.-]")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -54,8 +61,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}
|
||||
}
|
||||
|
@ -91,5 +98,15 @@ func updateSchedule(schedule *portainer.Schedule, payload *scheduleUpdatePayload
|
|||
updateJobSchedule = true
|
||||
}
|
||||
|
||||
if payload.RetryCount != nil {
|
||||
schedule.ScriptExecutionJob.RetryCount = *payload.RetryCount
|
||||
updateJobSchedule = true
|
||||
}
|
||||
|
||||
if payload.RetryInterval != nil {
|
||||
schedule.ScriptExecutionJob.RetryInterval = *payload.RetryInterval
|
||||
updateJobSchedule = true
|
||||
}
|
||||
|
||||
return updateJobSchedule
|
||||
}
|
||||
|
|
|
@ -9,6 +9,10 @@ import (
|
|||
"github.com/portainer/portainer/http/security"
|
||||
)
|
||||
|
||||
func hideFields(settings *portainer.Settings) {
|
||||
settings.LDAPSettings.Password = ""
|
||||
}
|
||||
|
||||
// Handler is the HTTP handler used to handle settings operations.
|
||||
type Handler struct {
|
||||
*mux.Router
|
||||
|
|
|
@ -14,5 +14,6 @@ func (handler *Handler) settingsInspect(w http.ResponseWriter, r *http.Request)
|
|||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve the settings from the database", err}
|
||||
}
|
||||
|
||||
hideFields(settings)
|
||||
return response.JSON(w, settings)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -111,12 +111,13 @@ func (handler *Handler) proxyWebsocketRequest(w http.ResponseWriter, r *http.Req
|
|||
}
|
||||
}
|
||||
|
||||
signature, err := handler.SignatureService.Sign(portainer.PortainerAgentSignatureMessage)
|
||||
signature, err := handler.SignatureService.CreateSignature()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
proxy.Director = func(incoming *http.Request, out http.Header) {
|
||||
out.Set(portainer.PortainerAgentPublicKeyHeader, handler.SignatureService.EncodedPublicKey())
|
||||
out.Set(portainer.PortainerAgentSignatureHeader, signature)
|
||||
out.Set(portainer.PortainerAgentTargetHeader, params.nodeName)
|
||||
}
|
||||
|
|
|
@ -64,7 +64,7 @@ func (p *proxyTransport) proxyDockerRequest(request *http.Request) (*http.Respon
|
|||
request.URL.Path = path
|
||||
|
||||
if p.enableSignature {
|
||||
signature, err := p.SignatureService.Sign(portainer.PortainerAgentSignatureMessage)
|
||||
signature, err := p.SignatureService.CreateSignature()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
|
@ -47,7 +47,7 @@ type (
|
|||
// LDAPSettings represents the settings used to connect to a LDAP server
|
||||
LDAPSettings struct {
|
||||
ReaderDN string `json:"ReaderDN"`
|
||||
Password string `json:"Password"`
|
||||
Password string `json:"Password,omitempty"`
|
||||
URL string `json:"URL"`
|
||||
TLSConfig TLSConfiguration `json:"TLSConfig"`
|
||||
StartTLS bool `json:"StartTLS"`
|
||||
|
@ -228,21 +228,18 @@ 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
|
||||
Endpoints []EndpointID
|
||||
Image string
|
||||
ScriptPath string
|
||||
RetryCount int
|
||||
RetryInterval int
|
||||
}
|
||||
|
||||
// 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
|
||||
|
@ -629,7 +626,7 @@ type (
|
|||
GenerateKeyPair() ([]byte, []byte, error)
|
||||
EncodedPublicKey() string
|
||||
PEMHeaders() (string, string)
|
||||
Sign(message string) (string, error)
|
||||
CreateSignature() (string, error)
|
||||
}
|
||||
|
||||
// JWTService represents a service for managing JWT tokens
|
||||
|
@ -666,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
|
||||
|
@ -708,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
|
||||
}
|
||||
)
|
||||
|
||||
|
@ -718,7 +714,7 @@ const (
|
|||
// DBVersion is the version number of the Portainer database
|
||||
DBVersion = 14
|
||||
// MessageOfTheDayURL represents the URL where Portainer MOTD message can be retrieved
|
||||
MessageOfTheDayURL = "https://raw.githubusercontent.com/portainer/motd/master/message.html"
|
||||
MessageOfTheDayURL = "https://portainer-io-assets.sfo2.digitaloceanspaces.com/motd.html"
|
||||
// PortainerAgentHeader represents the name of the header available in any agent response
|
||||
PortainerAgentHeader = "Portainer-Agent"
|
||||
// PortainerAgentTargetHeader represent the name of the header containing the target node name
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -6,7 +6,7 @@ angular.module('portainer.app')
|
|||
interceptor.responseError = responseErrorInterceptor;
|
||||
|
||||
function responseErrorInterceptor(rejection) {
|
||||
if (rejection.status === 502 || rejection.status === -1) {
|
||||
if (rejection.status === 502 || rejection.status === 503 || rejection.status === -1) {
|
||||
var endpoint = EndpointProvider.currentEndpoint();
|
||||
if (endpoint !== undefined) {
|
||||
var data = endpoint.Snapshots[0].SnapshotRaw.Containers;
|
||||
|
|
|
@ -6,7 +6,7 @@ angular.module('portainer.app')
|
|||
interceptor.responseError = responseErrorInterceptor;
|
||||
|
||||
function responseErrorInterceptor(rejection) {
|
||||
if (rejection.status === 502 || rejection.status === -1) {
|
||||
if (rejection.status === 502 || rejection.status === 503 || rejection.status === -1) {
|
||||
var endpoint = EndpointProvider.currentEndpoint();
|
||||
if (endpoint !== undefined) {
|
||||
var data = endpoint.Snapshots[0].SnapshotRaw.Images;
|
||||
|
|
|
@ -6,7 +6,7 @@ angular.module('portainer.app')
|
|||
interceptor.responseError = responseErrorInterceptor;
|
||||
|
||||
function responseErrorInterceptor(rejection) {
|
||||
if (rejection.status === 502 || rejection.status === -1) {
|
||||
if (rejection.status === 502 || rejection.status === 503 || rejection.status === -1) {
|
||||
var endpoint = EndpointProvider.currentEndpoint();
|
||||
if (endpoint !== undefined) {
|
||||
var data = endpoint.Snapshots[0].SnapshotRaw.Info;
|
||||
|
|
|
@ -6,7 +6,7 @@ angular.module('portainer.app')
|
|||
interceptor.responseError = responseErrorInterceptor;
|
||||
|
||||
function responseErrorInterceptor(rejection) {
|
||||
if (rejection.status === 502 || rejection.status === -1) {
|
||||
if (rejection.status === 502 || rejection.status === 503 || rejection.status === -1) {
|
||||
var endpoint = EndpointProvider.currentEndpoint();
|
||||
if (endpoint !== undefined) {
|
||||
var data = endpoint.Snapshots[0].SnapshotRaw.Networks;
|
||||
|
|
|
@ -6,7 +6,7 @@ angular.module('portainer.app')
|
|||
interceptor.responseError = responseErrorInterceptor;
|
||||
|
||||
function responseErrorInterceptor(rejection) {
|
||||
if (rejection.status === 502 || rejection.status === -1) {
|
||||
if (rejection.status === 502 || rejection.status === 503 || rejection.status === -1) {
|
||||
var endpoint = EndpointProvider.currentEndpoint();
|
||||
if (endpoint !== undefined) {
|
||||
var data = endpoint.Snapshots[0].SnapshotRaw.Version;
|
||||
|
|
|
@ -6,7 +6,7 @@ angular.module('portainer.app')
|
|||
interceptor.responseError = responseErrorInterceptor;
|
||||
|
||||
function responseErrorInterceptor(rejection) {
|
||||
if (rejection.status === 502 || rejection.status === -1) {
|
||||
if (rejection.status === 502 || rejection.status === 503 || rejection.status === -1) {
|
||||
var endpoint = EndpointProvider.currentEndpoint();
|
||||
if (endpoint !== undefined) {
|
||||
var data = endpoint.Snapshots[0].SnapshotRaw.Volumes;
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import _ from 'lodash';
|
||||
import { ResourceControlViewModel } from '../../portainer/models/resourceControl';
|
||||
|
||||
function createStatus(statusText) {
|
||||
export function createStatus(statusText) {
|
||||
var status = _.toLower(statusText);
|
||||
|
||||
if (status.indexOf('paused') > -1) {
|
||||
|
@ -74,8 +74,12 @@ export function ContainerStatsViewModel(data) {
|
|||
this.NumProcs = data.num_procs;
|
||||
this.isWindows = true;
|
||||
} else { // Linux
|
||||
this.MemoryUsage = data.memory_stats.usage - data.memory_stats.stats.cache;
|
||||
this.MemoryCache = data.memory_stats.stats.cache;
|
||||
if (data.memory_stats.stats === undefined || data.memory_stats.usage === undefined) {
|
||||
this.MemoryUsage = this.MemoryCache = 0;
|
||||
} else {
|
||||
this.MemoryUsage = data.memory_stats.usage - data.memory_stats.stats.cache;
|
||||
this.MemoryCache = data.memory_stats.stats.cache;
|
||||
}
|
||||
}
|
||||
this.PreviousCPUTotalUsage = data.precpu_stats.cpu_usage.total_usage;
|
||||
this.PreviousCPUSystemUsage = data.precpu_stats.system_cpu_usage;
|
||||
|
|
|
@ -156,6 +156,7 @@ function ($q, $scope, $state, $transition$, $filter, Commit, ContainerHelper, Co
|
|||
Commit.commitContainer({id: $transition$.params().id, tag: imageConfig.tag, repo: imageConfig.repo}, function () {
|
||||
update();
|
||||
Notifications.success('Container commited', $transition$.params().id);
|
||||
$scope.config.Image = '';
|
||||
}, function (e) {
|
||||
update();
|
||||
Notifications.error('Failure', e, 'Unable to commit container');
|
||||
|
|
|
@ -84,7 +84,7 @@
|
|||
<i class="fa fa-th-list"></i>
|
||||
</div>
|
||||
<div class="title">{{ stackCount }}</div>
|
||||
<div class="comment">Stacks</div>
|
||||
<div class="comment">{{ stackCount === 1 ? 'Stack' : 'Stacks' }}</div>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</a>
|
||||
|
@ -97,7 +97,7 @@
|
|||
<i class="fa fa-list-alt"></i>
|
||||
</div>
|
||||
<div class="title">{{ serviceCount }}</div>
|
||||
<div class="comment">Services</div>
|
||||
<div class="comment">{{ serviceCount === 1 ? 'Service' : 'Services' }}</div>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</a>
|
||||
|
@ -114,7 +114,7 @@
|
|||
<div><i class="fa fa-heartbeat space-right red-icon"></i>{{ containers | stoppedcontainers }} stopped</div>
|
||||
</div>
|
||||
<div class="title">{{ containers.length }}</div>
|
||||
<div class="comment">Containers</div>
|
||||
<div class="comment">{{ containers.length === 1 ? 'Container' : 'Containers' }}</div>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</a>
|
||||
|
@ -130,7 +130,7 @@
|
|||
<div><i class="fa fa-chart-pie space-right"></i>{{ images | imagestotalsize | humansize }}</div>
|
||||
</div>
|
||||
<div class="title">{{ images.length }}</div>
|
||||
<div class="comment">Images</div>
|
||||
<div class="comment">{{ images.length === 1 ? 'Image' : 'Images' }}</div>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</a>
|
||||
|
@ -143,7 +143,7 @@
|
|||
<i class="fa fa-cubes"></i>
|
||||
</div>
|
||||
<div class="title">{{ volumeCount }}</div>
|
||||
<div class="comment">Volumes</div>
|
||||
<div class="comment">{{ volumeCount === 1 ? 'Volume' : 'Volumes' }}</div>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</a>
|
||||
|
@ -156,7 +156,7 @@
|
|||
<i class="fa fa-sitemap"></i>
|
||||
</div>
|
||||
<div class="title">{{ networkCount }}</div>
|
||||
<div class="comment">Networks</div>
|
||||
<div class="comment">{{ networkCount === 1 ? 'Network' : 'Networks' }}</div>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</a>
|
||||
|
|
|
@ -108,7 +108,11 @@ angular.module('portainer.docker')
|
|||
config.ConfigFrom = {
|
||||
Network: selectedNetworkConfig.Name
|
||||
};
|
||||
config.Scope = 'swarm';
|
||||
if ($scope.applicationState.endpoint.mode.provider === 'DOCKER_SWARM_MODE') {
|
||||
config.Scope = 'swarm';
|
||||
} else {
|
||||
config.Scope = 'local';
|
||||
}
|
||||
}
|
||||
|
||||
function validateForm(accessControlData, isAdmin) {
|
||||
|
@ -195,9 +199,6 @@ angular.module('portainer.docker')
|
|||
|
||||
PluginService.networkPlugins(apiVersion < 1.25)
|
||||
.then(function success(data) {
|
||||
if ($scope.applicationState.endpoint.mode.provider !== 'DOCKER_SWARM_MODE') {
|
||||
data.splice(data.indexOf('macvlan'), 1);
|
||||
}
|
||||
$scope.availableNetworkDrivers = data;
|
||||
})
|
||||
.catch(function error(err) {
|
||||
|
|
|
@ -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: './scheduleTasksDatatable.html',
|
||||
controller: 'GenericDatatableController',
|
||||
bindings: {
|
||||
titleText: '@',
|
||||
titleIcon: '@',
|
||||
dataset: '<',
|
||||
tableKey: '@',
|
||||
orderBy: '@',
|
||||
reverseOrder: '<',
|
||||
goToContainerLogs: '<'
|
||||
}
|
||||
});
|
|
@ -38,13 +38,13 @@
|
|||
<span class="blocklist-item-desc">
|
||||
<span>
|
||||
<span style="padding: 0 7px 0 0;">
|
||||
<i class="fa fa-th-list space-right" aria-hidden="true"></i>{{ $ctrl.model.Snapshots[0].StackCount }} stacks
|
||||
<i class="fa fa-th-list space-right" aria-hidden="true"></i>{{ $ctrl.model.Snapshots[0].StackCount }} {{ $ctrl.model.Snapshots[0].StackCount === 1 ? 'stack' : 'stacks' }}
|
||||
</span>
|
||||
<span style="padding: 0 7px 0 7px;" ng-if="$ctrl.model.Snapshots[0].Swarm">
|
||||
<i class="fa fa-list-alt space-right" aria-hidden="true"></i>{{ $ctrl.model.Snapshots[0].ServiceCount }} services
|
||||
<i class="fa fa-list-alt space-right" aria-hidden="true"></i>{{ $ctrl.model.Snapshots[0].ServiceCount }} {{ $ctrl.model.Snapshots[0].ServiceCount === 1 ? 'service' : 'services' }}
|
||||
</span>
|
||||
<span style="padding: 0 7px 0 7px;">
|
||||
<i class="fa fa-server space-right" aria-hidden="true"></i>{{ $ctrl.model.Snapshots[0].RunningContainerCount + $ctrl.model.Snapshots[0].StoppedContainerCount }} containers
|
||||
<i class="fa fa-server space-right" aria-hidden="true"></i>{{ $ctrl.model.Snapshots[0].RunningContainerCount + $ctrl.model.Snapshots[0].StoppedContainerCount }} {{ $ctrl.model.Snapshots[0].RunningContainerCount + $ctrl.model.Snapshots[0].StoppedContainerCount === 1 ? 'container' : 'containers' }}
|
||||
<span ng-if="$ctrl.model.Snapshots[0].RunningContainerCount > 0 || $ctrl.model.Snapshots[0].StoppedContainerCount > 0">
|
||||
-
|
||||
<i class="fa fa-heartbeat green-icon" aria-hidden="true"></i> {{ $ctrl.model.Snapshots[0].RunningContainerCount }}
|
||||
|
@ -52,10 +52,10 @@
|
|||
</span>
|
||||
</span>
|
||||
<span style="padding: 0 7px 0 7px;">
|
||||
<i class="fa fa-cubes space-right" aria-hidden="true"></i>{{ $ctrl.model.Snapshots[0].VolumeCount }} volumes
|
||||
<i class="fa fa-cubes space-right" aria-hidden="true"></i>{{ $ctrl.model.Snapshots[0].VolumeCount }} {{ $ctrl.model.Snapshots[0].VolumeCount === 1 ? 'volume' : 'volumes' }}
|
||||
</span>
|
||||
<span style="padding: 0 7px 0 7px;">
|
||||
<i class="fa fa-clone space-right" aria-hidden="true"></i>{{ $ctrl.model.Snapshots[0].ImageCount }} images
|
||||
<i class="fa fa-clone space-right" aria-hidden="true"></i>{{ $ctrl.model.Snapshots[0].ImageCount }} {{ $ctrl.model.Snapshots[0].ImageCount === 1 ? 'image' : 'images' }}
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
|
@ -82,6 +82,9 @@
|
|||
</span>
|
||||
</span>
|
||||
</span>
|
||||
<span class="small text-muted">
|
||||
{{ $ctrl.model.URL | stripprotocol }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
</span>
|
||||
|
|
|
@ -46,6 +46,7 @@ angular.module('portainer.app').controller('EndpointListController', [
|
|||
return (
|
||||
_.includes(endpoint.Name.toLowerCase(), lowerCaseKeyword) ||
|
||||
_.includes(endpoint.GroupName.toLowerCase(), lowerCaseKeyword) ||
|
||||
_.includes(endpoint.URL.toLowerCase(), lowerCaseKeyword) ||
|
||||
_.some(endpoint.Tags, function(tag) {
|
||||
return _.includes(tag.toLowerCase(), lowerCaseKeyword);
|
||||
}) ||
|
||||
|
|
|
@ -21,7 +21,7 @@
|
|||
class="searchInput"
|
||||
ng-model="$ctrl.state.textFilter"
|
||||
ng-change="$ctrl.onFilterChanged()"
|
||||
placeholder="Search by name, group, tag, status..." auto-focus>
|
||||
placeholder="Search by name, group, tag, status, URL..." auto-focus>
|
||||
</div>
|
||||
|
||||
<div class="blocklist">
|
||||
|
|
|
@ -42,8 +42,8 @@
|
|||
</div>
|
||||
<!-- image-input -->
|
||||
<div class="form-group">
|
||||
<label for="schedule_image" class="col-sm-1 control-label text-left">Image</label>
|
||||
<div class="col-sm-11">
|
||||
<label for="schedule_image" class="col-sm-2 control-label text-left">Image</label>
|
||||
<div class="col-sm-10">
|
||||
<input type="text" class="form-control" ng-model="$ctrl.model.Job.Image" id="schedule_image" name="schedule_image" placeholder="e.g. ubuntu:latest" required>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -55,12 +55,24 @@
|
|||
</div>
|
||||
</div>
|
||||
<!-- !image-input -->
|
||||
<!-- retry-policy -->
|
||||
<div class="form-group">
|
||||
<span class="col-sm-12 text-muted small">
|
||||
This schedule will be executed via a privileged container on the target hosts. You can access the host filesystem under the
|
||||
<code>/host</code> folder.
|
||||
</span>
|
||||
<label for="retrycount" class="col-sm-2 control-label text-left">
|
||||
Retry count
|
||||
<portainer-tooltip position="bottom" message="Number of retries when it's not possible to reach the endpoint."></portainer-tooltip>
|
||||
</label>
|
||||
<div class="col-sm-10 col-md-4">
|
||||
<input type="number" class="form-control" ng-model="$ctrl.model.Job.RetryCount" id="retrycount" name="retrycount" placeholder="3">
|
||||
</div>
|
||||
<label for="retryinterval" class="col-sm-2 control-label text-left">
|
||||
Retry interval
|
||||
<portainer-tooltip position="bottom" message="Retry interval in seconds."></portainer-tooltip>
|
||||
</label>
|
||||
<div class="col-sm-10 col-md-4">
|
||||
<input type="number" class="form-control" ng-model="$ctrl.model.Job.RetryInterval" id="retryinterval" name="retryinterval" placeholder="30">
|
||||
</div>
|
||||
</div>
|
||||
<!-- !retry-policy -->
|
||||
<!-- execution-method -->
|
||||
<div ng-if="!$ctrl.model.Id">
|
||||
<div class="col-sm-12 form-section-title">
|
||||
|
@ -98,6 +110,12 @@
|
|||
<div class="col-sm-12 form-section-title">
|
||||
Web editor
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<span class="col-sm-12 text-muted small">
|
||||
This schedule will be executed via a privileged container on the target hosts. You can access the host filesystem under the
|
||||
<code>/host</code> folder.
|
||||
</span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<code-editor
|
||||
|
|
|
@ -4,5 +4,12 @@
|
|||
<i class="fa fa-exclamation-circle orange-icon" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||
This endpoint is currently offline (read-only). Data shown is based on the latest available snapshot.
|
||||
</p>
|
||||
<p class="text-muted">
|
||||
<i class="fa fa-clock" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||
Last snapshot: {{ $ctrl.snapshotTime | getisodatefromtimestamp }}
|
||||
</p>
|
||||
<button type="button" class="btn btn-xs btn-primary" ng-click="$ctrl.triggerSnapshot()" ng-if="$ctrl.showRefreshButton">
|
||||
<i class="fa fa-sync space-right" aria-hidden="true"></i>Refresh
|
||||
</button>
|
||||
</span>
|
||||
</information-panel>
|
||||
</information-panel>
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
angular.module('portainer.app').component('informationPanelOffline', {
|
||||
templateUrl: './informationPanelOffline.html',
|
||||
transclude: true
|
||||
controller: 'InformationPanelOfflineController'
|
||||
});
|
||||
|
|
|
@ -0,0 +1,34 @@
|
|||
angular.module('portainer.app').controller('InformationPanelOfflineController', ['$state', 'EndpointProvider', 'EndpointService', 'Authentication', 'Notifications',
|
||||
function StackDuplicationFormController($state, EndpointProvider, EndpointService, Authentication, Notifications) {
|
||||
var ctrl = this;
|
||||
|
||||
this.$onInit = onInit;
|
||||
this.triggerSnapshot = triggerSnapshot;
|
||||
|
||||
function triggerSnapshot() {
|
||||
var endpointId = EndpointProvider.endpointID();
|
||||
|
||||
EndpointService.snapshotEndpoint(endpointId)
|
||||
.then(function onSuccess() {
|
||||
$state.reload();
|
||||
})
|
||||
.catch(function onError(err) {
|
||||
Notifications.error('Failure', err, 'An error occured during endpoint snapshot');
|
||||
});
|
||||
}
|
||||
|
||||
function onInit() {
|
||||
var endpointId = EndpointProvider.endpointID();
|
||||
ctrl.showRefreshButton = Authentication.getUserDetails().role === 1;
|
||||
|
||||
|
||||
EndpointService.endpoint(endpointId)
|
||||
.then(function onSuccess(data) {
|
||||
ctrl.snapshotTime = data.Snapshots[0].Time;
|
||||
})
|
||||
.catch(function onError(err) {
|
||||
Notifications.error('Failure', err, 'Unable to retrieve endpoint information');
|
||||
});
|
||||
}
|
||||
|
||||
}]);
|
|
@ -32,7 +32,7 @@ angular.module('portainer.app')
|
|||
function responseErrorInterceptor(rejection) {
|
||||
var EndpointService = $injector.get('EndpointService');
|
||||
var url = rejection.config.url;
|
||||
if ((rejection.status === 502 || rejection.status === -1) && canBeOffline(url) && !EndpointProvider.offlineMode()) {
|
||||
if ((rejection.status === 502 || rejection.status === 503 || rejection.status === -1) && canBeOffline(url) && !EndpointProvider.offlineMode()) {
|
||||
EndpointProvider.setOfflineMode(true);
|
||||
EndpointService.updateEndpoint(EndpointProvider.endpointID(), {Status: EndpointProvider.endpointStatusFromOfflineMode(true)});
|
||||
}
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import { createStatus } from '../../docker/models/container';
|
||||
|
||||
export function ScheduleDefaultModel() {
|
||||
this.Name = '';
|
||||
this.CronExpression = '';
|
||||
|
@ -29,6 +31,15 @@ function ScriptExecutionJobModel(data) {
|
|||
this.Endpoints = data.Endpoints;
|
||||
this.FileContent = '';
|
||||
this.Method = 'editor';
|
||||
this.RetryCount = data.RetryCount;
|
||||
this.RetryInterval = data.RetryInterval;
|
||||
}
|
||||
|
||||
export function ScriptExecutionTaskModel(data) {
|
||||
this.Id = data.Id;
|
||||
this.EndpointId = data.EndpointId;
|
||||
this.Status = createStatus(data.Status);
|
||||
this.Created = data.Created;
|
||||
}
|
||||
|
||||
export function ScheduleCreateRequest(model) {
|
||||
|
@ -37,6 +48,8 @@ export function ScheduleCreateRequest(model) {
|
|||
this.Image = model.Job.Image;
|
||||
this.Endpoints = model.Job.Endpoints;
|
||||
this.FileContent = model.Job.FileContent;
|
||||
this.RetryCount = model.Job.RetryCount;
|
||||
this.RetryInterval = model.Job.RetryInterval;
|
||||
this.File = model.Job.File;
|
||||
}
|
||||
|
||||
|
@ -47,4 +60,6 @@ export function ScheduleUpdateRequest(model) {
|
|||
this.Image = model.Job.Image;
|
||||
this.Endpoints = model.Job.Endpoints;
|
||||
this.FileContent = model.Job.FileContent;
|
||||
this.RetryCount = model.Job.RetryCount;
|
||||
this.RetryInterval = model.Job.RetryInterval;
|
||||
}
|
||||
|
|
|
@ -7,7 +7,8 @@ angular.module('portainer.app')
|
|||
update: { method: 'PUT', params: { id: '@id' } },
|
||||
updateAccess: { method: 'PUT', params: { id: '@id', action: 'access' } },
|
||||
remove: { method: 'DELETE', params: { id: '@id'} },
|
||||
snapshot: { method: 'POST', params: { id: 'snapshot' }},
|
||||
snapshots: { method: 'POST', params: { action: 'snapshot' }},
|
||||
snapshot: { method: 'POST', params: { id: '@id', action: 'snapshot' }},
|
||||
executeJob: { method: 'POST', ignoreLoadingBar: true, params: { id: '@id', action: 'job' } }
|
||||
});
|
||||
}]);
|
||||
|
|
|
@ -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' } }
|
||||
});
|
||||
}]);
|
||||
|
|
|
@ -12,8 +12,12 @@ function EndpointServiceFactory($q, Endpoints, FileUploadService) {
|
|||
return Endpoints.query({}).$promise;
|
||||
};
|
||||
|
||||
service.snapshot = function() {
|
||||
return Endpoints.snapshot({}, {}).$promise;
|
||||
service.snapshotEndpoints = function() {
|
||||
return Endpoints.snapshots({}, {}).$promise;
|
||||
};
|
||||
|
||||
service.snapshotEndpoint = function(endpointID) {
|
||||
return Endpoints.snapshot({ id: endpointID }, {}).$promise;
|
||||
};
|
||||
|
||||
service.endpointsByGroup = function(groupId) {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { ScheduleModel, ScheduleCreateRequest, ScheduleUpdateRequest } from '../../models/schedule';
|
||||
import { ScheduleModel, ScheduleCreateRequest, ScheduleUpdateRequest, ScriptExecutionTaskModel } from '../../models/schedule';
|
||||
|
||||
angular.module('portainer.app')
|
||||
.factory('ScheduleService', ['$q', 'Schedules', 'FileUploadService',
|
||||
|
@ -38,6 +38,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;
|
||||
|
|
|
@ -49,7 +49,9 @@ angular.module('portainer.app')
|
|||
Name: payload.Name,
|
||||
CronExpression: payload.CronExpression,
|
||||
Image: payload.Image,
|
||||
Endpoints: Upload.json(payload.Endpoints)
|
||||
Endpoints: Upload.json(payload.Endpoints),
|
||||
RetryCount: payload.RetryCount,
|
||||
RetryInterval: payload.RetryInterval
|
||||
}
|
||||
});
|
||||
};
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
</rd-header>
|
||||
|
||||
<information-panel
|
||||
ng-if="motd && applicationState.UI.dismissedInfoHash !== motd.Hash"
|
||||
ng-if="motd && motd.Message !== '' && applicationState.UI.dismissedInfoHash !== motd.Hash"
|
||||
title-text="Important message"
|
||||
dismiss-action="dismissImportantInformation(motd.Hash)">
|
||||
<span class="text-muted">
|
||||
|
|
|
@ -101,7 +101,7 @@ function ($q, $scope, $state, Authentication, EndpointService, EndpointHelper, G
|
|||
}
|
||||
|
||||
function triggerSnapshot() {
|
||||
EndpointService.snapshot()
|
||||
EndpointService.snapshotEndpoints()
|
||||
.then(function success() {
|
||||
Notifications.success('Success', 'Endpoints updated');
|
||||
$state.reload();
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -168,13 +168,13 @@ a[ng-click]{
|
|||
padding: 0.7rem;
|
||||
margin-bottom: 0.7rem;
|
||||
cursor: pointer;
|
||||
border: 1px solid #333333;
|
||||
border: 1px solid #cccccc;
|
||||
border-radius: 2px;
|
||||
box-shadow: 0 3px 10px -2px rgba(161, 170, 166, 0.5);
|
||||
}
|
||||
|
||||
.blocklist-item--selected {
|
||||
border: 2px solid #333333;
|
||||
border: 2px solid #bbbbbb;
|
||||
background-color: #ececec;
|
||||
color: #2d3e63;
|
||||
}
|
||||
|
|
|
@ -65,7 +65,7 @@
|
|||
"splitargs": "github:deviantony/splitargs#~0.2.0",
|
||||
"toastr": "^2.1.4",
|
||||
"ui-select": "^0.19.8",
|
||||
"xterm": "^3.1.0"
|
||||
"xterm": "^3.8.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.1.2",
|
||||
|
|
|
@ -9483,10 +9483,10 @@ xtend@~3.0.0:
|
|||
resolved "https://registry.yarnpkg.com/xtend/-/xtend-3.0.0.tgz#5cce7407baf642cba7becda568111c493f59665a"
|
||||
integrity sha1-XM50B7r2Qsunvs2laBEcST9ZZlo=
|
||||
|
||||
xterm@^3.1.0:
|
||||
version "3.1.0"
|
||||
resolved "https://registry.yarnpkg.com/xterm/-/xterm-3.1.0.tgz#7f7e1c8cf4b80bd881a4e8891213b851423e90c9"
|
||||
integrity sha512-xv4QQQAu8PjAPZE7m0h+i9X9z3vR82wPe0H3g4D9CbyDuQDNt1e+lxeTrkLwuMH3jcxqfHayu3Mu+iyQh62ECg==
|
||||
xterm@^3.8.0:
|
||||
version "3.8.0"
|
||||
resolved "https://registry.yarnpkg.com/xterm/-/xterm-3.8.0.tgz#55d1de518bdc9c9793823f5e4e97d6898972938d"
|
||||
integrity sha512-rS3HLryuMWbLsv98+jVVSUXCxmoyXPwqwJNC0ad0VSMdXgl65LefPztQVwfurkaF7kM7ZSgM8eJjnJ9kkdoR1w==
|
||||
|
||||
"y18n@^3.2.1 || ^4.0.0", y18n@^4.0.0:
|
||||
version "4.0.0"
|
||||
|
|
Loading…
Reference in New Issue