mirror of https://github.com/portainer/portainer
feat(home): add a new home view (#2033)
parent
a94f2ee7b8
commit
b6792461a4
|
@ -21,6 +21,7 @@ const (
|
|||
errEndpointsFileNotFound = portainer.Error("Unable to locate external endpoints file")
|
||||
errTemplateFileNotFound = portainer.Error("Unable to locate template file on disk")
|
||||
errInvalidSyncInterval = portainer.Error("Invalid synchronization interval")
|
||||
errInvalidSnapshotInterval = portainer.Error("Invalid snapshot interval")
|
||||
errEndpointExcludeExternal = portainer.Error("Cannot use the -H flag mutually with --external-endpoints")
|
||||
errNoAuthExcludeAdminPassword = portainer.Error("Cannot use --no-auth with --admin-password or --admin-password-file")
|
||||
errAdminPassExcludeAdminPassFile = portainer.Error("Cannot use --admin-password with --admin-password-file")
|
||||
|
@ -47,6 +48,8 @@ func (*Service) ParseFlags(version string) (*portainer.CLIFlags, error) {
|
|||
SSLCert: kingpin.Flag("sslcert", "Path to the SSL certificate used to secure the Portainer instance").Default(defaultSSLCertPath).String(),
|
||||
SSLKey: kingpin.Flag("sslkey", "Path to the SSL key used to secure the Portainer instance").Default(defaultSSLKeyPath).String(),
|
||||
SyncInterval: kingpin.Flag("sync-interval", "Duration between each synchronization via the external endpoints source").Default(defaultSyncInterval).String(),
|
||||
Snapshot: kingpin.Flag("snapshot", "Start a background job to create endpoint snapshots").Default(defaultSnapshot).Bool(),
|
||||
SnapshotInterval: kingpin.Flag("snapshot-interval", "Duration between each endpoint snapshot job").Default(defaultSnapshotInterval).String(),
|
||||
AdminPassword: kingpin.Flag("admin-password", "Hashed admin password").String(),
|
||||
AdminPasswordFile: kingpin.Flag("admin-password-file", "Path to the file containing the password for the admin user").String(),
|
||||
Labels: pairs(kingpin.Flag("hide-label", "Hide containers with a specific label in the UI").Short('l')),
|
||||
|
@ -95,6 +98,11 @@ func (*Service) ValidateFlags(flags *portainer.CLIFlags) error {
|
|||
return err
|
||||
}
|
||||
|
||||
err = validateSnapshotInterval(*flags.SnapshotInterval)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if *flags.NoAuth && (*flags.AdminPassword != "" || *flags.AdminPasswordFile != "") {
|
||||
return errNoAuthExcludeAdminPassword
|
||||
}
|
||||
|
@ -156,3 +164,13 @@ func validateSyncInterval(syncInterval string) error {
|
|||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateSnapshotInterval(snapshotInterval string) error {
|
||||
if snapshotInterval != defaultSnapshotInterval {
|
||||
_, err := time.ParseDuration(snapshotInterval)
|
||||
if err != nil {
|
||||
return errInvalidSnapshotInterval
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -3,19 +3,21 @@
|
|||
package cli
|
||||
|
||||
const (
|
||||
defaultBindAddress = ":9000"
|
||||
defaultDataDirectory = "/data"
|
||||
defaultAssetsDirectory = "./"
|
||||
defaultNoAuth = "false"
|
||||
defaultNoAnalytics = "false"
|
||||
defaultTLS = "false"
|
||||
defaultTLSSkipVerify = "false"
|
||||
defaultTLSCACertPath = "/certs/ca.pem"
|
||||
defaultTLSCertPath = "/certs/cert.pem"
|
||||
defaultTLSKeyPath = "/certs/key.pem"
|
||||
defaultSSL = "false"
|
||||
defaultSSLCertPath = "/certs/portainer.crt"
|
||||
defaultSSLKeyPath = "/certs/portainer.key"
|
||||
defaultSyncInterval = "60s"
|
||||
defaultTemplateFile = "/templates.json"
|
||||
defaultBindAddress = ":9000"
|
||||
defaultDataDirectory = "/data"
|
||||
defaultAssetsDirectory = "./"
|
||||
defaultNoAuth = "false"
|
||||
defaultNoAnalytics = "false"
|
||||
defaultTLS = "false"
|
||||
defaultTLSSkipVerify = "false"
|
||||
defaultTLSCACertPath = "/certs/ca.pem"
|
||||
defaultTLSCertPath = "/certs/cert.pem"
|
||||
defaultTLSKeyPath = "/certs/key.pem"
|
||||
defaultSSL = "false"
|
||||
defaultSSLCertPath = "/certs/portainer.crt"
|
||||
defaultSSLKeyPath = "/certs/portainer.key"
|
||||
defaultSyncInterval = "60s"
|
||||
defaultSnapshot = "true"
|
||||
defaultSnapshotInterval = "5m"
|
||||
defaultTemplateFile = "/templates.json"
|
||||
)
|
||||
|
|
|
@ -1,19 +1,21 @@
|
|||
package cli
|
||||
|
||||
const (
|
||||
defaultBindAddress = ":9000"
|
||||
defaultDataDirectory = "C:\\data"
|
||||
defaultAssetsDirectory = "./"
|
||||
defaultNoAuth = "false"
|
||||
defaultNoAnalytics = "false"
|
||||
defaultTLS = "false"
|
||||
defaultTLSSkipVerify = "false"
|
||||
defaultTLSCACertPath = "C:\\certs\\ca.pem"
|
||||
defaultTLSCertPath = "C:\\certs\\cert.pem"
|
||||
defaultTLSKeyPath = "C:\\certs\\key.pem"
|
||||
defaultSSL = "false"
|
||||
defaultSSLCertPath = "C:\\certs\\portainer.crt"
|
||||
defaultSSLKeyPath = "C:\\certs\\portainer.key"
|
||||
defaultSyncInterval = "60s"
|
||||
defaultTemplateFile = "/templates.json"
|
||||
defaultBindAddress = ":9000"
|
||||
defaultDataDirectory = "C:\\data"
|
||||
defaultAssetsDirectory = "./"
|
||||
defaultNoAuth = "false"
|
||||
defaultNoAnalytics = "false"
|
||||
defaultTLS = "false"
|
||||
defaultTLSSkipVerify = "false"
|
||||
defaultTLSCACertPath = "C:\\certs\\ca.pem"
|
||||
defaultTLSCertPath = "C:\\certs\\cert.pem"
|
||||
defaultTLSKeyPath = "C:\\certs\\key.pem"
|
||||
defaultSSL = "false"
|
||||
defaultSSLCertPath = "C:\\certs\\portainer.crt"
|
||||
defaultSSLKeyPath = "C:\\certs\\portainer.key"
|
||||
defaultSyncInterval = "60s"
|
||||
defaultSnapshot = "true"
|
||||
defaultSnapshotInterval = "5m"
|
||||
defaultTemplateFile = "/templates.json"
|
||||
)
|
||||
|
|
|
@ -9,6 +9,7 @@ import (
|
|||
"github.com/portainer/portainer/cli"
|
||||
"github.com/portainer/portainer/cron"
|
||||
"github.com/portainer/portainer/crypto"
|
||||
"github.com/portainer/portainer/docker"
|
||||
"github.com/portainer/portainer/exec"
|
||||
"github.com/portainer/portainer/filesystem"
|
||||
"github.com/portainer/portainer/git"
|
||||
|
@ -101,25 +102,37 @@ func initGitService() portainer.GitService {
|
|||
return &git.Service{}
|
||||
}
|
||||
|
||||
func initEndpointWatcher(endpointService portainer.EndpointService, externalEnpointFile string, syncInterval string) bool {
|
||||
authorizeEndpointMgmt := true
|
||||
if externalEnpointFile != "" {
|
||||
authorizeEndpointMgmt = false
|
||||
log.Println("Using external endpoint definition. Endpoint management via the API will be disabled.")
|
||||
endpointWatcher := cron.NewWatcher(endpointService, syncInterval)
|
||||
err := endpointWatcher.WatchEndpointFile(externalEnpointFile)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
return authorizeEndpointMgmt
|
||||
func initClientFactory(signatureService portainer.DigitalSignatureService) *docker.ClientFactory {
|
||||
return docker.NewClientFactory(signatureService)
|
||||
}
|
||||
|
||||
func initStatus(authorizeEndpointMgmt bool, flags *portainer.CLIFlags) *portainer.Status {
|
||||
func initJobScheduler(endpointService portainer.EndpointService, clientFactory *docker.ClientFactory, flags *portainer.CLIFlags) (portainer.JobScheduler, error) {
|
||||
jobScheduler := cron.NewJobScheduler(endpointService, clientFactory)
|
||||
|
||||
if *flags.ExternalEndpoints != "" {
|
||||
log.Println("Using external endpoint definition. Endpoint management via the API will be disabled.")
|
||||
err := jobScheduler.ScheduleEndpointSyncJob(*flags.ExternalEndpoints, *flags.SyncInterval)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if *flags.Snapshot {
|
||||
err := jobScheduler.ScheduleSnapshotJob(*flags.SnapshotInterval)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return jobScheduler, nil
|
||||
}
|
||||
|
||||
func initStatus(endpointManagement, snapshot bool, flags *portainer.CLIFlags) *portainer.Status {
|
||||
return &portainer.Status{
|
||||
Analytics: !*flags.NoAnalytics,
|
||||
Authentication: !*flags.NoAuth,
|
||||
EndpointManagement: authorizeEndpointMgmt,
|
||||
EndpointManagement: endpointManagement,
|
||||
Snapshot: snapshot,
|
||||
Version: portainer.APIVersion,
|
||||
}
|
||||
}
|
||||
|
@ -154,6 +167,7 @@ func initSettings(settingsService portainer.SettingsService, flags *portainer.CL
|
|||
},
|
||||
AllowBindMountsForRegularUsers: true,
|
||||
AllowPrivilegedModeForRegularUsers: true,
|
||||
SnapshotInterval: *flags.SnapshotInterval,
|
||||
}
|
||||
|
||||
if *flags.Labels != nil {
|
||||
|
@ -283,6 +297,8 @@ func createTLSSecuredEndpoint(flags *portainer.CLIFlags, endpointService portain
|
|||
AuthorizedTeams: []portainer.TeamID{},
|
||||
Extensions: []portainer.EndpointExtension{},
|
||||
Tags: []string{},
|
||||
Status: portainer.EndpointStatusUp,
|
||||
Snapshots: []portainer.Snapshot{},
|
||||
}
|
||||
|
||||
if strings.HasPrefix(endpoint.URL, "tcp://") {
|
||||
|
@ -322,6 +338,8 @@ func createUnsecuredEndpoint(endpointURL string, endpointService portainer.Endpo
|
|||
AuthorizedTeams: []portainer.TeamID{},
|
||||
Extensions: []portainer.EndpointExtension{},
|
||||
Tags: []string{},
|
||||
Status: portainer.EndpointStatusUp,
|
||||
Snapshots: []portainer.Snapshot{},
|
||||
}
|
||||
|
||||
return endpointService.CreateEndpoint(endpoint)
|
||||
|
@ -366,9 +384,21 @@ func main() {
|
|||
|
||||
gitService := initGitService()
|
||||
|
||||
authorizeEndpointMgmt := initEndpointWatcher(store.EndpointService, *flags.ExternalEndpoints, *flags.SyncInterval)
|
||||
clientFactory := initClientFactory(digitalSignatureService)
|
||||
|
||||
err := initKeyPair(fileService, digitalSignatureService)
|
||||
jobScheduler, err := initJobScheduler(store.EndpointService, clientFactory, flags)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
jobScheduler.Start()
|
||||
|
||||
endpointManagement := true
|
||||
if *flags.ExternalEndpoints != "" {
|
||||
endpointManagement = false
|
||||
}
|
||||
|
||||
err = initKeyPair(fileService, digitalSignatureService)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
@ -395,7 +425,7 @@ func main() {
|
|||
log.Fatal(err)
|
||||
}
|
||||
|
||||
applicationStatus := initStatus(authorizeEndpointMgmt, flags)
|
||||
applicationStatus := initStatus(endpointManagement, *flags.Snapshot, flags)
|
||||
|
||||
err = initEndpoint(flags, store.EndpointService)
|
||||
if err != nil {
|
||||
|
@ -443,7 +473,7 @@ func main() {
|
|||
BindAddress: *flags.Addr,
|
||||
AssetsPath: *flags.Assets,
|
||||
AuthDisabled: *flags.NoAuth,
|
||||
EndpointManagement: authorizeEndpointMgmt,
|
||||
EndpointManagement: endpointManagement,
|
||||
UserService: store.UserService,
|
||||
TeamService: store.TeamService,
|
||||
TeamMembershipService: store.TeamMembershipService,
|
||||
|
@ -464,6 +494,7 @@ func main() {
|
|||
LDAPService: ldapService,
|
||||
GitService: gitService,
|
||||
SignatureService: digitalSignatureService,
|
||||
JobScheduler: jobScheduler,
|
||||
SSL: *flags.SSL,
|
||||
SSLCert: *flags.SSLCert,
|
||||
SSLKey: *flags.SSLKey,
|
||||
|
|
|
@ -0,0 +1,60 @@
|
|||
package cron
|
||||
|
||||
import (
|
||||
"log"
|
||||
|
||||
"github.com/portainer/portainer"
|
||||
)
|
||||
|
||||
type (
|
||||
endpointSnapshotJob struct {
|
||||
endpointService portainer.EndpointService
|
||||
snapshotter portainer.Snapshotter
|
||||
}
|
||||
)
|
||||
|
||||
func newEndpointSnapshotJob(endpointService portainer.EndpointService, snapshotter portainer.Snapshotter) endpointSnapshotJob {
|
||||
return endpointSnapshotJob{
|
||||
endpointService: endpointService,
|
||||
snapshotter: snapshotter,
|
||||
}
|
||||
}
|
||||
|
||||
func (job endpointSnapshotJob) Snapshot() error {
|
||||
|
||||
endpoints, err := job.endpointService.Endpoints()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, endpoint := range endpoints {
|
||||
if endpoint.Type == portainer.AzureEnvironment {
|
||||
continue
|
||||
}
|
||||
|
||||
snapshot, err := job.snapshotter.CreateSnapshot(&endpoint)
|
||||
endpoint.Status = portainer.EndpointStatusUp
|
||||
if err != nil {
|
||||
log.Printf("cron error: endpoint snapshot error (endpoint=%s, URL=%s) (err=%s)\n", endpoint.Name, endpoint.URL, err)
|
||||
endpoint.Status = portainer.EndpointStatusDown
|
||||
}
|
||||
|
||||
if snapshot != nil {
|
||||
endpoint.Snapshots = []portainer.Snapshot{*snapshot}
|
||||
}
|
||||
|
||||
err = job.endpointService.UpdateEndpoint(endpoint.ID, &endpoint)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (job endpointSnapshotJob) Run() {
|
||||
err := job.Snapshot()
|
||||
if err != nil {
|
||||
log.Printf("cron error: snapshot job error (err=%s)\n", err)
|
||||
}
|
||||
}
|
|
@ -4,7 +4,6 @@ import (
|
|||
"encoding/json"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/portainer/portainer"
|
||||
|
@ -12,7 +11,6 @@ import (
|
|||
|
||||
type (
|
||||
endpointSyncJob struct {
|
||||
logger *log.Logger
|
||||
endpointService portainer.EndpointService
|
||||
endpointFilePath string
|
||||
}
|
||||
|
@ -41,15 +39,14 @@ const (
|
|||
|
||||
func newEndpointSyncJob(endpointFilePath string, endpointService portainer.EndpointService) endpointSyncJob {
|
||||
return endpointSyncJob{
|
||||
logger: log.New(os.Stderr, "", log.LstdFlags),
|
||||
endpointService: endpointService,
|
||||
endpointFilePath: endpointFilePath,
|
||||
}
|
||||
}
|
||||
|
||||
func endpointSyncError(err error, logger *log.Logger) bool {
|
||||
func endpointSyncError(err error) bool {
|
||||
if err != nil {
|
||||
logger.Printf("Endpoint synchronization error: %s", err)
|
||||
log.Printf("cron error: synchronization job error (err=%s)\n", err)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
|
@ -140,23 +137,23 @@ func (job endpointSyncJob) prepareSyncData(storedEndpoints, fileEndpoints []port
|
|||
if fidx != -1 {
|
||||
endpoint := mergeEndpointIfRequired(&storedEndpoints[idx], &fileEndpoints[fidx])
|
||||
if endpoint != nil {
|
||||
job.logger.Printf("New definition for a stored endpoint found in file, updating database. [name: %v] [url: %v]\n", endpoint.Name, endpoint.URL)
|
||||
log.Printf("New definition for a stored endpoint found in file, updating database. [name: %v] [url: %v]\n", endpoint.Name, endpoint.URL)
|
||||
endpointsToUpdate = append(endpointsToUpdate, endpoint)
|
||||
}
|
||||
} else {
|
||||
job.logger.Printf("Stored endpoint not found in file (definition might be invalid), removing from database. [name: %v] [url: %v]", storedEndpoints[idx].Name, storedEndpoints[idx].URL)
|
||||
log.Printf("Stored endpoint not found in file (definition might be invalid), removing from database. [name: %v] [url: %v]", storedEndpoints[idx].Name, storedEndpoints[idx].URL)
|
||||
endpointsToDelete = append(endpointsToDelete, &storedEndpoints[idx])
|
||||
}
|
||||
}
|
||||
|
||||
for idx, endpoint := range fileEndpoints {
|
||||
if !isValidEndpoint(&endpoint) {
|
||||
job.logger.Printf("Invalid file endpoint definition, skipping. [name: %v] [url: %v]", endpoint.Name, endpoint.URL)
|
||||
log.Printf("Invalid file endpoint definition, skipping. [name: %v] [url: %v]", endpoint.Name, endpoint.URL)
|
||||
continue
|
||||
}
|
||||
sidx := endpointExists(&fileEndpoints[idx], storedEndpoints)
|
||||
if sidx == -1 {
|
||||
job.logger.Printf("File endpoint not found in database, adding to database. [name: %v] [url: %v]", fileEndpoints[idx].Name, fileEndpoints[idx].URL)
|
||||
log.Printf("File endpoint not found in database, adding to database. [name: %v] [url: %v]", fileEndpoints[idx].Name, fileEndpoints[idx].URL)
|
||||
endpointsToCreate = append(endpointsToCreate, &fileEndpoints[idx])
|
||||
}
|
||||
}
|
||||
|
@ -170,13 +167,13 @@ func (job endpointSyncJob) prepareSyncData(storedEndpoints, fileEndpoints []port
|
|||
|
||||
func (job endpointSyncJob) Sync() error {
|
||||
data, err := ioutil.ReadFile(job.endpointFilePath)
|
||||
if endpointSyncError(err, job.logger) {
|
||||
if endpointSyncError(err) {
|
||||
return err
|
||||
}
|
||||
|
||||
var fileEndpoints []fileEndpoint
|
||||
err = json.Unmarshal(data, &fileEndpoints)
|
||||
if endpointSyncError(err, job.logger) {
|
||||
if endpointSyncError(err) {
|
||||
return err
|
||||
}
|
||||
|
||||
|
@ -185,7 +182,7 @@ func (job endpointSyncJob) Sync() error {
|
|||
}
|
||||
|
||||
storedEndpoints, err := job.endpointService.Endpoints()
|
||||
if endpointSyncError(err, job.logger) {
|
||||
if endpointSyncError(err) {
|
||||
return err
|
||||
}
|
||||
|
||||
|
@ -194,16 +191,16 @@ func (job endpointSyncJob) Sync() error {
|
|||
sync := job.prepareSyncData(storedEndpoints, convertedFileEndpoints)
|
||||
if sync.requireSync() {
|
||||
err = job.endpointService.Synchronize(sync.endpointsToCreate, sync.endpointsToUpdate, sync.endpointsToDelete)
|
||||
if endpointSyncError(err, job.logger) {
|
||||
if endpointSyncError(err) {
|
||||
return err
|
||||
}
|
||||
job.logger.Printf("Endpoint synchronization ended. [created: %v] [updated: %v] [deleted: %v]", len(sync.endpointsToCreate), len(sync.endpointsToUpdate), len(sync.endpointsToDelete))
|
||||
log.Printf("Endpoint synchronization ended. [created: %v] [updated: %v] [deleted: %v]", len(sync.endpointsToCreate), len(sync.endpointsToUpdate), len(sync.endpointsToDelete))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (job endpointSyncJob) Run() {
|
||||
job.logger.Println("Endpoint synchronization job started.")
|
||||
log.Println("cron: synchronization job started")
|
||||
err := job.Sync()
|
||||
endpointSyncError(err, job.logger)
|
||||
endpointSyncError(err)
|
||||
}
|
|
@ -0,0 +1,87 @@
|
|||
package cron
|
||||
|
||||
import (
|
||||
"log"
|
||||
|
||||
"github.com/portainer/portainer"
|
||||
"github.com/portainer/portainer/docker"
|
||||
"github.com/robfig/cron"
|
||||
)
|
||||
|
||||
// JobScheduler represents a service for managing crons.
|
||||
type JobScheduler struct {
|
||||
cron *cron.Cron
|
||||
endpointService portainer.EndpointService
|
||||
snapshotter portainer.Snapshotter
|
||||
|
||||
endpointFilePath string
|
||||
endpointSyncInterval string
|
||||
}
|
||||
|
||||
// NewJobScheduler initializes a new service.
|
||||
func NewJobScheduler(endpointService portainer.EndpointService, clientFactory *docker.ClientFactory) *JobScheduler {
|
||||
return &JobScheduler{
|
||||
cron: cron.New(),
|
||||
endpointService: endpointService,
|
||||
snapshotter: docker.NewSnapshotter(clientFactory),
|
||||
}
|
||||
}
|
||||
|
||||
// ScheduleEndpointSyncJob schedules a cron job to synchronize the endpoints from a file
|
||||
func (scheduler *JobScheduler) ScheduleEndpointSyncJob(endpointFilePath string, interval string) error {
|
||||
|
||||
scheduler.endpointFilePath = endpointFilePath
|
||||
scheduler.endpointSyncInterval = interval
|
||||
|
||||
job := newEndpointSyncJob(endpointFilePath, scheduler.endpointService)
|
||||
|
||||
err := job.Sync()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return scheduler.cron.AddJob("@every "+interval, job)
|
||||
}
|
||||
|
||||
// ScheduleSnapshotJob schedules a cron job to create endpoint snapshots
|
||||
func (scheduler *JobScheduler) ScheduleSnapshotJob(interval string) error {
|
||||
job := newEndpointSnapshotJob(scheduler.endpointService, scheduler.snapshotter)
|
||||
|
||||
err := job.Snapshot()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return scheduler.cron.AddJob("@every "+interval, job)
|
||||
}
|
||||
|
||||
// UpdateSnapshotJob will update the schedules to match the new snapshot interval
|
||||
func (scheduler *JobScheduler) UpdateSnapshotJob(interval string) {
|
||||
// TODO: the cron library do not support removing/updating schedules.
|
||||
// As a work-around we need to re-create the cron and reschedule the jobs.
|
||||
// We should update the library.
|
||||
jobs := scheduler.cron.Entries()
|
||||
scheduler.cron.Stop()
|
||||
|
||||
scheduler.cron = cron.New()
|
||||
|
||||
for _, job := range jobs {
|
||||
switch job.Job.(type) {
|
||||
case endpointSnapshotJob:
|
||||
scheduler.ScheduleSnapshotJob(interval)
|
||||
case endpointSyncJob:
|
||||
scheduler.ScheduleEndpointSyncJob(scheduler.endpointFilePath, scheduler.endpointSyncInterval)
|
||||
default:
|
||||
log.Println("Unsupported job")
|
||||
}
|
||||
}
|
||||
|
||||
scheduler.cron.Start()
|
||||
}
|
||||
|
||||
// Start starts the scheduled jobs
|
||||
func (scheduler *JobScheduler) Start() {
|
||||
if len(scheduler.cron.Entries()) > 0 {
|
||||
scheduler.cron.Start()
|
||||
}
|
||||
}
|
|
@ -1,40 +0,0 @@
|
|||
package cron
|
||||
|
||||
import (
|
||||
"github.com/portainer/portainer"
|
||||
"github.com/robfig/cron"
|
||||
)
|
||||
|
||||
// Watcher represents a service for managing crons.
|
||||
type Watcher struct {
|
||||
Cron *cron.Cron
|
||||
EndpointService portainer.EndpointService
|
||||
syncInterval string
|
||||
}
|
||||
|
||||
// NewWatcher initializes a new service.
|
||||
func NewWatcher(endpointService portainer.EndpointService, syncInterval string) *Watcher {
|
||||
return &Watcher{
|
||||
Cron: cron.New(),
|
||||
EndpointService: endpointService,
|
||||
syncInterval: syncInterval,
|
||||
}
|
||||
}
|
||||
|
||||
// WatchEndpointFile starts a cron job to synchronize the endpoints from a file
|
||||
func (watcher *Watcher) WatchEndpointFile(endpointFilePath string) error {
|
||||
job := newEndpointSyncJob(endpointFilePath, watcher.EndpointService)
|
||||
|
||||
err := job.Sync()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = watcher.Cron.AddJob("@every "+watcher.syncInterval, job)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
watcher.Cron.Start()
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,103 @@
|
|||
package docker
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/portainer/portainer"
|
||||
"github.com/portainer/portainer/crypto"
|
||||
)
|
||||
|
||||
const (
|
||||
unsupportedEnvironmentType = portainer.Error("Environment not supported")
|
||||
)
|
||||
|
||||
// ClientFactory is used to create Docker clients
|
||||
type ClientFactory struct {
|
||||
signatureService portainer.DigitalSignatureService
|
||||
}
|
||||
|
||||
// NewClientFactory returns a new instance of a ClientFactory
|
||||
func NewClientFactory(signatureService portainer.DigitalSignatureService) *ClientFactory {
|
||||
return &ClientFactory{
|
||||
signatureService: signatureService,
|
||||
}
|
||||
}
|
||||
|
||||
// CreateClient is a generic function to create a Docker client based on
|
||||
// a specific endpoint configuration
|
||||
func (factory *ClientFactory) CreateClient(endpoint *portainer.Endpoint) (*client.Client, error) {
|
||||
if endpoint.Type == portainer.AzureEnvironment {
|
||||
return nil, unsupportedEnvironmentType
|
||||
} else if endpoint.Type == portainer.AgentOnDockerEnvironment {
|
||||
return createAgentClient(endpoint, factory.signatureService)
|
||||
}
|
||||
|
||||
if strings.HasPrefix(endpoint.URL, "unix://") {
|
||||
return createUnixSocketClient(endpoint)
|
||||
}
|
||||
return createTCPClient(endpoint)
|
||||
}
|
||||
|
||||
func createUnixSocketClient(endpoint *portainer.Endpoint) (*client.Client, error) {
|
||||
return client.NewClientWithOpts(
|
||||
client.WithHost(endpoint.URL),
|
||||
client.WithVersion(portainer.SupportedDockerAPIVersion),
|
||||
)
|
||||
}
|
||||
|
||||
func createTCPClient(endpoint *portainer.Endpoint) (*client.Client, error) {
|
||||
httpCli, err := httpClient(endpoint)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return client.NewClientWithOpts(
|
||||
client.WithHost(endpoint.URL),
|
||||
client.WithVersion(portainer.SupportedDockerAPIVersion),
|
||||
client.WithHTTPClient(httpCli),
|
||||
)
|
||||
}
|
||||
|
||||
func createAgentClient(endpoint *portainer.Endpoint, signatureService portainer.DigitalSignatureService) (*client.Client, error) {
|
||||
httpCli, err := httpClient(endpoint)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
signature, err := signatureService.Sign(portainer.PortainerAgentSignatureMessage)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
headers := map[string]string{
|
||||
portainer.PortainerAgentPublicKeyHeader: signatureService.EncodedPublicKey(),
|
||||
portainer.PortainerAgentSignatureHeader: signature,
|
||||
}
|
||||
|
||||
return client.NewClientWithOpts(
|
||||
client.WithHost(endpoint.URL),
|
||||
client.WithVersion(portainer.SupportedDockerAPIVersion),
|
||||
client.WithHTTPClient(httpCli),
|
||||
client.WithHTTPHeaders(headers),
|
||||
)
|
||||
}
|
||||
|
||||
func httpClient(endpoint *portainer.Endpoint) (*http.Client, error) {
|
||||
transport := &http.Transport{}
|
||||
|
||||
if endpoint.TLSConfig.TLS {
|
||||
tlsConfig, err := crypto.CreateTLSConfigurationFromDisk(endpoint.TLSConfig.TLSCACertPath, endpoint.TLSConfig.TLSCertPath, endpoint.TLSConfig.TLSKeyPath, endpoint.TLSConfig.TLSSkipVerify)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
transport.TLSClientConfig = tlsConfig
|
||||
}
|
||||
|
||||
return &http.Client{
|
||||
Timeout: time.Second * 10,
|
||||
Transport: transport,
|
||||
}, nil
|
||||
}
|
|
@ -0,0 +1,135 @@
|
|||
package docker
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/portainer/portainer"
|
||||
)
|
||||
|
||||
func snapshot(cli *client.Client) (*portainer.Snapshot, error) {
|
||||
_, err := cli.Ping(context.Background())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
snapshot := &portainer.Snapshot{
|
||||
StackCount: 0,
|
||||
}
|
||||
|
||||
err = snapshotInfo(snapshot, cli)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if snapshot.Swarm {
|
||||
err = snapshotSwarmServices(snapshot, cli)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
err = snapshotContainers(snapshot, cli)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = snapshotImages(snapshot, cli)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = snapshotVolumes(snapshot, cli)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
snapshot.Time = time.Now().Unix()
|
||||
return snapshot, nil
|
||||
}
|
||||
|
||||
func snapshotInfo(snapshot *portainer.Snapshot, cli *client.Client) error {
|
||||
info, err := cli.Info(context.Background())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
snapshot.Swarm = info.Swarm.ControlAvailable
|
||||
snapshot.DockerVersion = info.ServerVersion
|
||||
snapshot.TotalCPU = info.NCPU
|
||||
snapshot.TotalMemory = info.MemTotal
|
||||
return nil
|
||||
}
|
||||
|
||||
func snapshotSwarmServices(snapshot *portainer.Snapshot, cli *client.Client) error {
|
||||
stacks := make(map[string]struct{})
|
||||
|
||||
services, err := cli.ServiceList(context.Background(), types.ServiceListOptions{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, service := range services {
|
||||
for k, v := range service.Spec.Labels {
|
||||
if k == "com.docker.stack.namespace" {
|
||||
stacks[v] = struct{}{}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
snapshot.ServiceCount = len(services)
|
||||
snapshot.StackCount += len(stacks)
|
||||
return nil
|
||||
}
|
||||
|
||||
func snapshotContainers(snapshot *portainer.Snapshot, cli *client.Client) error {
|
||||
containers, err := cli.ContainerList(context.Background(), types.ContainerListOptions{All: true})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
runningContainers := 0
|
||||
stoppedContainers := 0
|
||||
stacks := make(map[string]struct{})
|
||||
for _, container := range containers {
|
||||
if container.State == "exited" {
|
||||
stoppedContainers++
|
||||
} else if container.State == "running" {
|
||||
runningContainers++
|
||||
}
|
||||
|
||||
for k, v := range container.Labels {
|
||||
if k == "com.docker.compose.project" {
|
||||
stacks[v] = struct{}{}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
snapshot.RunningContainerCount = runningContainers
|
||||
snapshot.StoppedContainerCount = stoppedContainers
|
||||
snapshot.StackCount += len(stacks)
|
||||
return nil
|
||||
}
|
||||
|
||||
func snapshotImages(snapshot *portainer.Snapshot, cli *client.Client) error {
|
||||
images, err := cli.ImageList(context.Background(), types.ImageListOptions{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
snapshot.ImageCount = len(images)
|
||||
return nil
|
||||
}
|
||||
|
||||
func snapshotVolumes(snapshot *portainer.Snapshot, cli *client.Client) error {
|
||||
volumes, err := cli.VolumeList(context.Background(), filters.Args{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
snapshot.VolumeCount = len(volumes.Volumes)
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
package docker
|
||||
|
||||
import (
|
||||
"github.com/portainer/portainer"
|
||||
)
|
||||
|
||||
// Snapshotter represents a service used to create endpoint snapshots
|
||||
type Snapshotter struct {
|
||||
clientFactory *ClientFactory
|
||||
}
|
||||
|
||||
// NewSnapshotter returns a new Snapshotter instance
|
||||
func NewSnapshotter(clientFactory *ClientFactory) *Snapshotter {
|
||||
return &Snapshotter{
|
||||
clientFactory: clientFactory,
|
||||
}
|
||||
}
|
||||
|
||||
// CreateSnapshot creates a snapshot of a specific endpoint
|
||||
func (snapshotter *Snapshotter) CreateSnapshot(endpoint *portainer.Endpoint) (*portainer.Snapshot, error) {
|
||||
cli, err := snapshotter.clientFactory.CreateClient(endpoint)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return snapshot(cli)
|
||||
}
|
|
@ -177,6 +177,8 @@ func (handler *Handler) createAzureEndpoint(payload *endpointCreatePayload) (*po
|
|||
Extensions: []portainer.EndpointExtension{},
|
||||
AzureCredentials: credentials,
|
||||
Tags: payload.Tags,
|
||||
Status: portainer.EndpointStatusUp,
|
||||
Snapshots: []portainer.Snapshot{},
|
||||
}
|
||||
|
||||
err = handler.EndpointService.CreateEndpoint(endpoint)
|
||||
|
@ -213,6 +215,8 @@ func (handler *Handler) createUnsecuredEndpoint(payload *endpointCreatePayload)
|
|||
AuthorizedTeams: []portainer.TeamID{},
|
||||
Extensions: []portainer.EndpointExtension{},
|
||||
Tags: payload.Tags,
|
||||
Status: portainer.EndpointStatusUp,
|
||||
Snapshots: []portainer.Snapshot{},
|
||||
}
|
||||
|
||||
err := handler.EndpointService.CreateEndpoint(endpoint)
|
||||
|
@ -253,6 +257,8 @@ func (handler *Handler) createTLSSecuredEndpoint(payload *endpointCreatePayload)
|
|||
AuthorizedTeams: []portainer.TeamID{},
|
||||
Extensions: []portainer.EndpointExtension{},
|
||||
Tags: payload.Tags,
|
||||
Status: portainer.EndpointStatusUp,
|
||||
Snapshots: []portainer.Snapshot{},
|
||||
}
|
||||
|
||||
err = handler.EndpointService.CreateEndpoint(endpoint)
|
||||
|
|
|
@ -15,6 +15,7 @@ type Handler struct {
|
|||
SettingsService portainer.SettingsService
|
||||
LDAPService portainer.LDAPService
|
||||
FileService portainer.FileService
|
||||
JobScheduler portainer.JobScheduler
|
||||
}
|
||||
|
||||
// NewHandler creates a handler to manage settings operations.
|
||||
|
|
|
@ -13,6 +13,7 @@ type publicSettingsResponse struct {
|
|||
AuthenticationMethod portainer.AuthenticationMethod `json:"AuthenticationMethod"`
|
||||
AllowBindMountsForRegularUsers bool `json:"AllowBindMountsForRegularUsers"`
|
||||
AllowPrivilegedModeForRegularUsers bool `json:"AllowPrivilegedModeForRegularUsers"`
|
||||
SnapshotInterval string `json:"SnapshotInterval"`
|
||||
}
|
||||
|
||||
// GET request on /api/settings/public
|
||||
|
@ -27,6 +28,7 @@ func (handler *Handler) settingsPublic(w http.ResponseWriter, r *http.Request) *
|
|||
AuthenticationMethod: settings.AuthenticationMethod,
|
||||
AllowBindMountsForRegularUsers: settings.AllowBindMountsForRegularUsers,
|
||||
AllowPrivilegedModeForRegularUsers: settings.AllowPrivilegedModeForRegularUsers,
|
||||
SnapshotInterval: settings.SnapshotInterval,
|
||||
}
|
||||
|
||||
return response.JSON(w, publicSettings)
|
||||
|
|
|
@ -12,22 +12,20 @@ import (
|
|||
)
|
||||
|
||||
type settingsUpdatePayload struct {
|
||||
LogoURL string
|
||||
LogoURL *string
|
||||
BlackListedLabels []portainer.Pair
|
||||
AuthenticationMethod int
|
||||
LDAPSettings portainer.LDAPSettings
|
||||
AllowBindMountsForRegularUsers bool
|
||||
AllowPrivilegedModeForRegularUsers bool
|
||||
AuthenticationMethod *int
|
||||
LDAPSettings *portainer.LDAPSettings
|
||||
AllowBindMountsForRegularUsers *bool
|
||||
AllowPrivilegedModeForRegularUsers *bool
|
||||
SnapshotInterval *string
|
||||
}
|
||||
|
||||
func (payload *settingsUpdatePayload) Validate(r *http.Request) error {
|
||||
if payload.AuthenticationMethod == 0 {
|
||||
return portainer.Error("Invalid authentication method")
|
||||
}
|
||||
if payload.AuthenticationMethod != 1 && payload.AuthenticationMethod != 2 {
|
||||
if *payload.AuthenticationMethod != 1 && *payload.AuthenticationMethod != 2 {
|
||||
return portainer.Error("Invalid authentication method value. Value must be one of: 1 (internal) or 2 (LDAP/AD)")
|
||||
}
|
||||
if !govalidator.IsNull(payload.LogoURL) && !govalidator.IsURL(payload.LogoURL) {
|
||||
if payload.LogoURL != nil && *payload.LogoURL != "" && !govalidator.IsURL(*payload.LogoURL) {
|
||||
return portainer.Error("Invalid logo URL. Must correspond to a valid URL format")
|
||||
}
|
||||
return nil
|
||||
|
@ -41,15 +39,40 @@ func (handler *Handler) settingsUpdate(w http.ResponseWriter, r *http.Request) *
|
|||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err}
|
||||
}
|
||||
|
||||
settings := &portainer.Settings{
|
||||
LogoURL: payload.LogoURL,
|
||||
BlackListedLabels: payload.BlackListedLabels,
|
||||
LDAPSettings: payload.LDAPSettings,
|
||||
AllowBindMountsForRegularUsers: payload.AllowBindMountsForRegularUsers,
|
||||
AllowPrivilegedModeForRegularUsers: payload.AllowPrivilegedModeForRegularUsers,
|
||||
settings, err := handler.SettingsService.Settings()
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve the settings from the database", err}
|
||||
}
|
||||
|
||||
if payload.AuthenticationMethod != nil {
|
||||
settings.AuthenticationMethod = portainer.AuthenticationMethod(*payload.AuthenticationMethod)
|
||||
}
|
||||
|
||||
if payload.LogoURL != nil {
|
||||
settings.LogoURL = *payload.LogoURL
|
||||
}
|
||||
|
||||
if payload.BlackListedLabels != nil {
|
||||
settings.BlackListedLabels = payload.BlackListedLabels
|
||||
}
|
||||
|
||||
if payload.LDAPSettings != nil {
|
||||
settings.LDAPSettings = *payload.LDAPSettings
|
||||
}
|
||||
|
||||
if payload.AllowBindMountsForRegularUsers != nil {
|
||||
settings.AllowBindMountsForRegularUsers = *payload.AllowBindMountsForRegularUsers
|
||||
}
|
||||
|
||||
if payload.AllowPrivilegedModeForRegularUsers != nil {
|
||||
settings.AllowPrivilegedModeForRegularUsers = *payload.AllowPrivilegedModeForRegularUsers
|
||||
}
|
||||
|
||||
if payload.SnapshotInterval != nil && *payload.SnapshotInterval != settings.SnapshotInterval {
|
||||
settings.SnapshotInterval = *payload.SnapshotInterval
|
||||
handler.JobScheduler.UpdateSnapshotJob(settings.SnapshotInterval)
|
||||
}
|
||||
|
||||
settings.AuthenticationMethod = portainer.AuthenticationMethod(payload.AuthenticationMethod)
|
||||
tlsError := handler.updateTLS(settings)
|
||||
if tlsError != nil {
|
||||
return tlsError
|
||||
|
|
|
@ -40,6 +40,7 @@ type Server struct {
|
|||
ComposeStackManager portainer.ComposeStackManager
|
||||
CryptoService portainer.CryptoService
|
||||
SignatureService portainer.DigitalSignatureService
|
||||
JobScheduler portainer.JobScheduler
|
||||
DockerHubService portainer.DockerHubService
|
||||
EndpointService portainer.EndpointService
|
||||
EndpointGroupService portainer.EndpointGroupService
|
||||
|
@ -120,6 +121,7 @@ func (server *Server) Start() error {
|
|||
settingsHandler.SettingsService = server.SettingsService
|
||||
settingsHandler.LDAPService = server.LDAPService
|
||||
settingsHandler.FileService = server.FileService
|
||||
settingsHandler.JobScheduler = server.JobScheduler
|
||||
|
||||
var stackHandler = stacks.NewHandler(requestBouncer)
|
||||
stackHandler.FileService = server.FileService
|
||||
|
|
|
@ -31,12 +31,15 @@ type (
|
|||
SSLCert *string
|
||||
SSLKey *string
|
||||
SyncInterval *string
|
||||
Snapshot *bool
|
||||
SnapshotInterval *string
|
||||
}
|
||||
|
||||
// Status represents the application status.
|
||||
Status struct {
|
||||
Authentication bool `json:"Authentication"`
|
||||
EndpointManagement bool `json:"EndpointManagement"`
|
||||
Snapshot bool `json:"Snapshot"`
|
||||
Analytics bool `json:"Analytics"`
|
||||
Version string `json:"Version"`
|
||||
}
|
||||
|
@ -75,6 +78,8 @@ type (
|
|||
LDAPSettings LDAPSettings `json:"LDAPSettings"`
|
||||
AllowBindMountsForRegularUsers bool `json:"AllowBindMountsForRegularUsers"`
|
||||
AllowPrivilegedModeForRegularUsers bool `json:"AllowPrivilegedModeForRegularUsers"`
|
||||
SnapshotInterval string `json:"SnapshotInterval"`
|
||||
|
||||
// Deprecated fields
|
||||
DisplayDonationHeader bool
|
||||
DisplayExternalContributors bool
|
||||
|
@ -177,6 +182,9 @@ type (
|
|||
// EndpointType represents the type of an endpoint.
|
||||
EndpointType int
|
||||
|
||||
// EndpointStatus represents the status of an endpoint
|
||||
EndpointStatus int
|
||||
|
||||
// Endpoint represents a Docker endpoint with all the info required
|
||||
// to connect to it.
|
||||
Endpoint struct {
|
||||
|
@ -192,6 +200,8 @@ type (
|
|||
Extensions []EndpointExtension `json:"Extensions"`
|
||||
AzureCredentials AzureCredentials `json:"AzureCredentials,omitempty"`
|
||||
Tags []string `json:"Tags"`
|
||||
Status EndpointStatus `json:"Status"`
|
||||
Snapshots []Snapshot `json:"Snapshots"`
|
||||
|
||||
// Deprecated fields
|
||||
// Deprecated in DBVersion == 4
|
||||
|
@ -209,6 +219,21 @@ type (
|
|||
AuthenticationKey string `json:"AuthenticationKey"`
|
||||
}
|
||||
|
||||
// Snapshot represents a snapshot of a specific endpoint at a specific time
|
||||
Snapshot struct {
|
||||
Time int64 `json:"Time"`
|
||||
DockerVersion string `json:"DockerVersion"`
|
||||
Swarm bool `json:"Swarm"`
|
||||
TotalCPU int `json:"TotalCPU"`
|
||||
TotalMemory int64 `json:"TotalMemory"`
|
||||
RunningContainerCount int `json:"RunningContainerCount"`
|
||||
StoppedContainerCount int `json:"StoppedContainerCount"`
|
||||
VolumeCount int `json:"VolumeCount"`
|
||||
ImageCount int `json:"ImageCount"`
|
||||
ServiceCount int `json:"ServiceCount"`
|
||||
StackCount int `json:"StackCount"`
|
||||
}
|
||||
|
||||
// EndpointGroupID represents an endpoint group identifier.
|
||||
EndpointGroupID int
|
||||
|
||||
|
@ -539,9 +564,17 @@ type (
|
|||
ClonePrivateRepositoryWithBasicAuth(repositoryURL, destination, username, password string) error
|
||||
}
|
||||
|
||||
// EndpointWatcher represents a service to synchronize the endpoints via an external source.
|
||||
EndpointWatcher interface {
|
||||
WatchEndpointFile(endpointFilePath string) error
|
||||
// JobScheduler represents a service to run jobs on a periodic basis.
|
||||
JobScheduler interface {
|
||||
ScheduleEndpointSyncJob(endpointFilePath, interval string) error
|
||||
ScheduleSnapshotJob(interval string) error
|
||||
UpdateSnapshotJob(interval string)
|
||||
Start()
|
||||
}
|
||||
|
||||
// Snapshotter represents a service used to create endpoint snapshots.
|
||||
Snapshotter interface {
|
||||
CreateSnapshot(endpoint *Endpoint) (*Snapshot, error)
|
||||
}
|
||||
|
||||
// LDAPService represents a service used to authenticate users against a LDAP/AD.
|
||||
|
@ -581,6 +614,8 @@ const (
|
|||
// PortainerAgentSignatureMessage represents the message used to create a digital signature
|
||||
// to be used when communicating with an agent
|
||||
PortainerAgentSignatureMessage = "Portainer-App"
|
||||
// SupportedDockerAPIVersion is the minimum Docker API version supported by Portainer.
|
||||
SupportedDockerAPIVersion = "1.24"
|
||||
)
|
||||
|
||||
const (
|
||||
|
@ -673,3 +708,11 @@ const (
|
|||
// ComposeStackTemplate represents a template used to deploy a Compose stack
|
||||
ComposeStackTemplate
|
||||
)
|
||||
|
||||
const (
|
||||
_ EndpointStatus = iota
|
||||
// EndpointStatusUp is used to represent an available endpoint
|
||||
EndpointStatusUp
|
||||
// EndpointStatusDown is used to represent an unavailable endpoint
|
||||
EndpointStatusDown
|
||||
)
|
||||
|
|
|
@ -4,9 +4,6 @@ angular.module('portainer.docker').component('dockerSidebarContent', {
|
|||
'endpointApiVersion': '<',
|
||||
'swarmManagement': '<',
|
||||
'standaloneManagement': '<',
|
||||
'adminAccess': '<',
|
||||
'externalContributions': '<',
|
||||
'sidebarToggledOn': '<',
|
||||
'currentState': '<'
|
||||
'adminAccess': '<'
|
||||
}
|
||||
});
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<rd-header>
|
||||
<rd-header-title title-text="Home"></rd-header-title>
|
||||
<rd-header-content>Dashboard</rd-header-content>
|
||||
<rd-header-title title-text="Dashboard"></rd-header-title>
|
||||
<rd-header-content>Endpoint summary</rd-header-content>
|
||||
</rd-header>
|
||||
|
||||
<div class="row" ng-if="applicationState.endpoint.mode.agentProxy">
|
||||
|
|
|
@ -187,6 +187,17 @@ angular.module('portainer.app', [])
|
|||
}
|
||||
};
|
||||
|
||||
var home = {
|
||||
name: 'portainer.home',
|
||||
url: '/home',
|
||||
views: {
|
||||
'content@': {
|
||||
templateUrl: 'app/portainer/views/home/home.html',
|
||||
controller: 'HomeController'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var registries = {
|
||||
name: 'portainer.registries',
|
||||
url: '/registries',
|
||||
|
@ -404,6 +415,7 @@ angular.module('portainer.app', [])
|
|||
$stateRegistryProvider.register(group);
|
||||
$stateRegistryProvider.register(groupAccess);
|
||||
$stateRegistryProvider.register(groupCreation);
|
||||
$stateRegistryProvider.register(home);
|
||||
$stateRegistryProvider.register(registries);
|
||||
$stateRegistryProvider.register(registry);
|
||||
$stateRegistryProvider.register(registryAccess);
|
||||
|
|
|
@ -30,8 +30,9 @@
|
|||
}
|
||||
|
||||
.datatable .searchBar {
|
||||
border-top: 1px solid #f6f6f6;
|
||||
padding: 10px;
|
||||
border-top: 1px solid #d2d1d1;
|
||||
border-bottom: 1px solid #d2d1d1;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.datatable .searchInput {
|
||||
|
|
|
@ -0,0 +1,113 @@
|
|||
<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 by name, group, tag..." auto-focus>
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
<a ng-click="$ctrl.changeOrderBy('Name')">
|
||||
Name
|
||||
<i class="fa fa-sort-alpha-down" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Name' && !$ctrl.state.reverseOrder"></i>
|
||||
<i class="fa fa-sort-alpha-up" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Name' && $ctrl.state.reverseOrder"></i>
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a ng-click="$ctrl.changeOrderBy('GroupName')">
|
||||
Group
|
||||
<i class="fa fa-sort-alpha-down" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'GroupName' && !$ctrl.state.reverseOrder"></i>
|
||||
<i class="fa fa-sort-alpha-up" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'GroupName' && $ctrl.state.reverseOrder"></i>
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a ng-click="$ctrl.changeOrderBy('Status')">
|
||||
Status
|
||||
<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('Type')">
|
||||
Type
|
||||
<i class="fa fa-sort-alpha-down" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Type' && !$ctrl.state.reverseOrder"></i>
|
||||
<i class="fa fa-sort-alpha-up" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Type' && $ctrl.state.reverseOrder"></i>
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a ng-click="$ctrl.changeOrderBy('Snapshots[0].Time')">
|
||||
Last snapshot
|
||||
<i class="fa fa-sort-alpha-down" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Snapshots[0].Time' && !$ctrl.state.reverseOrder"></i>
|
||||
<i class="fa fa-sort-alpha-up" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Snapshots[0].Time' && $ctrl.state.reverseOrder"></i>
|
||||
</a>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr dir-paginate-start="item in ($ctrl.state.filteredDataSet = ($ctrl.dataset | filter:$ctrl.state.textFilter | orderBy:$ctrl.state.orderBy:$ctrl.state.reverseOrder | itemsPerPage: $ctrl.state.paginatedItemLimit))" ng-class="{active: item.Checked}">
|
||||
<td>
|
||||
<a ng-click="$ctrl.dashboardAction(item)"><i class="fa fa-sign-in-alt" aria-hidden="true"></i> {{ item.Name }}</a>
|
||||
</td>
|
||||
<td>{{ item.GroupName }}</td>
|
||||
<td>
|
||||
<span class="label label-{{ item.Status|endpointstatusbadge }}">{{ item.Status === 1 ? 'up' : 'down' }}</span>
|
||||
</td>
|
||||
<td>
|
||||
<span>
|
||||
<i ng-class="item.Type | endpointtypeicon" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||
{{ item.Type | endpointtypename }}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<span ng-if="item.Snapshots.length > 0">
|
||||
{{ item.Snapshots[0].Time | getisodatefromtimestamp }}
|
||||
</span>
|
||||
<span ng-if="item.Snapshots.length === 0">-</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr dir-paginate-end class="text-muted" ng-if="item.Snapshots.length > 0">
|
||||
<td colspan="5" style="border: 0; text-align:center;">
|
||||
<snapshot-details
|
||||
snapshot="item.Snapshots[0]"
|
||||
></snapshot-details
|
||||
</td>
|
||||
</tr>
|
||||
<tr ng-if="!$ctrl.dataset">
|
||||
<td colspan="5" class="text-center text-muted">Loading...</td>
|
||||
</tr>
|
||||
<tr ng-if="$ctrl.state.filteredDataSet.length === 0">
|
||||
<td colspan="5" class="text-center text-muted">No endpoint 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>
|
|
@ -0,0 +1,13 @@
|
|||
angular.module('portainer.app').component('endpointsSnapshotDatatable', {
|
||||
templateUrl: 'app/portainer/components/datatables/endpoints-snapshot-datatable/endpointsSnapshotDatatable.html',
|
||||
controller: 'GenericDatatableController',
|
||||
bindings: {
|
||||
titleText: '@',
|
||||
titleIcon: '@',
|
||||
dataset: '<',
|
||||
tableKey: '@',
|
||||
orderBy: '@',
|
||||
reverseOrder: '<',
|
||||
dashboardAction: '<'
|
||||
}
|
||||
});
|
|
@ -0,0 +1,29 @@
|
|||
<span style="border-top: 2px solid #e2e2e2; padding: 7px;">
|
||||
<span style="padding: 0 7px 0 7px;">
|
||||
<i class="fa fa-th-list space-right" aria-hidden="true"></i>{{ $ctrl.snapshot.StackCount }} stacks
|
||||
</span>
|
||||
<span style="padding: 0 7px 0 7px;" ng-if="$ctrl.snapshot.Swarm">
|
||||
<i class="fa fa-list-alt space-right" aria-hidden="true"></i>{{ $ctrl.snapshot.ServiceCount }} services
|
||||
</span>
|
||||
<span style="padding: 0 7px 0 7px;">
|
||||
<i class="fa fa-server space-right" aria-hidden="true"></i>{{ $ctrl.snapshot.RunningContainerCount + $ctrl.snapshot.StoppedContainerCount }} containers
|
||||
<span ng-if="$ctrl.snapshot.RunningContainerCount > 0 || $ctrl.snapshot.StoppedContainerCount > 0">
|
||||
-
|
||||
<i class="fa fa-heartbeat green-icon" aria-hidden="true"></i> {{ $ctrl.snapshot.RunningContainerCount }}
|
||||
<i class="fa fa-heartbeat red-icon" aria-hidden="true"></i> {{ $ctrl.snapshot.StoppedContainerCount }}
|
||||
</span>
|
||||
</span>
|
||||
<span style="padding: 0 7px 0 7px;">
|
||||
<i class="fa fa-cubes space-right" aria-hidden="true"></i>{{ $ctrl.snapshot.VolumeCount }} volumes
|
||||
</span>
|
||||
<span style="padding: 0 7px 0 7px;">
|
||||
<i class="fa fa-clone space-right" aria-hidden="true"></i>{{ $ctrl.snapshot.ImageCount }} images
|
||||
</span>
|
||||
<span style="padding: 0 7px 0 7px; border-left: 2px solid #e2e2e2;">
|
||||
<i class="fa fa-memory" aria-hidden="true"></i> {{ $ctrl.snapshot.TotalMemory | humansize }}
|
||||
<i class="fa fa-microchip space-left" aria-hidden="true"></i> {{ $ctrl.snapshot.TotalCPU }}
|
||||
</span>
|
||||
<span style="padding: 0 7px 0 7px; border-left: 2px solid #e2e2e2;">
|
||||
{{ $ctrl.snapshot.Swarm ? 'Swarm' : 'Standalone' }} {{ $ctrl.snapshot.DockerVersion }}
|
||||
</span>
|
||||
</span>
|
|
@ -0,0 +1,6 @@
|
|||
angular.module('portainer.app').component('snapshotDetails', {
|
||||
templateUrl: 'app/portainer/components/datatables/endpoints-snapshot-datatable/snapshot-details/snapshotDetails.html',
|
||||
bindings: {
|
||||
snapshot: '<'
|
||||
}
|
||||
});
|
|
@ -0,0 +1,7 @@
|
|||
angular.module('portainer.app').component('informationPanel', {
|
||||
templateUrl: 'app/portainer/components/information-panel/informationPanel.html',
|
||||
bindings: {
|
||||
titleText: '@'
|
||||
},
|
||||
transclude: true
|
||||
});
|
|
@ -0,0 +1,14 @@
|
|||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
<rd-widget>
|
||||
<rd-widget-body>
|
||||
<div class="col-sm-12 form-section-title">
|
||||
{{ $ctrl.titleText }}
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<ng-transclude></ng-transclude>
|
||||
</div>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
</div>
|
|
@ -1,9 +0,0 @@
|
|||
angular.module('portainer.app').component('sidebarEndpointSelector', {
|
||||
templateUrl: 'app/portainer/components/sidebar-endpoint-selector/sidebarEndpointSelector.html',
|
||||
controller: 'SidebarEndpointSelectorController',
|
||||
bindings: {
|
||||
'endpoints': '<',
|
||||
'groups': '<',
|
||||
'selectEndpoint': '<'
|
||||
}
|
||||
});
|
|
@ -1,27 +0,0 @@
|
|||
<div ng-if="$ctrl.endpoints.length > 1">
|
||||
<div ng-if="!$ctrl.state.show">
|
||||
<li class="sidebar-title">
|
||||
<span class="interactive" style="color: #fff;" ng-click="$ctrl.state.show = true;">
|
||||
<span class="fa fa-plug space-right"></span>Change environment
|
||||
</span>
|
||||
</li>
|
||||
</div>
|
||||
<div ng-if="$ctrl.state.show">
|
||||
<div ng-if="$ctrl.availableGroups.length > 1">
|
||||
<li class="sidebar-title"><span>Group</span></li>
|
||||
<li class="sidebar-title">
|
||||
<select class="select-endpoint form-control" ng-options="group.Name for group in $ctrl.availableGroups" ng-model="$ctrl.state.selectedGroup" ng-change="$ctrl.selectGroup()">
|
||||
<option value="" disabled selected>Select a group</option>
|
||||
</select>
|
||||
</li>
|
||||
</div>
|
||||
<div ng-if="$ctrl.state.selectedGroup || $ctrl.availableGroups.length <= 1">
|
||||
<li class="sidebar-title"><span>Endpoint</span></li>
|
||||
<li class="sidebar-title">
|
||||
<select class="select-endpoint form-control" ng-options="endpoint.Name for endpoint in $ctrl.availableEndpoints" ng-model="$ctrl.state.selectedEndpoint" ng-change="$ctrl.selectEndpoint($ctrl.state.selectedEndpoint)">
|
||||
<option value="" disabled selected>Select an endpoint</option>
|
||||
</select>
|
||||
</li>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
|
@ -1,34 +0,0 @@
|
|||
angular.module('portainer.app')
|
||||
.controller('SidebarEndpointSelectorController', function () {
|
||||
var ctrl = this;
|
||||
|
||||
this.state = {
|
||||
show: false,
|
||||
selectedGroup: null,
|
||||
selectedEndpoint: null
|
||||
};
|
||||
|
||||
this.selectGroup = function() {
|
||||
this.availableEndpoints = this.endpoints.filter(function f(endpoint) {
|
||||
return endpoint.GroupId === ctrl.state.selectedGroup.Id;
|
||||
});
|
||||
};
|
||||
|
||||
this.$onInit = function() {
|
||||
this.availableGroups = filterEmptyGroups(this.groups, this.endpoints);
|
||||
this.availableEndpoints = this.endpoints;
|
||||
};
|
||||
|
||||
function filterEmptyGroups(groups, endpoints) {
|
||||
return groups.filter(function f(group) {
|
||||
for (var i = 0; i < endpoints.length; i++) {
|
||||
|
||||
var endpoint = endpoints[i];
|
||||
if (endpoint.GroupId === group.Id) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
});
|
|
@ -138,4 +138,13 @@ angular.module('portainer.app')
|
|||
return 'fa fa-eye';
|
||||
}
|
||||
};
|
||||
})
|
||||
.filter('endpointstatusbadge', function () {
|
||||
'use strict';
|
||||
return function (status) {
|
||||
if (status === 2) {
|
||||
return 'danger';
|
||||
}
|
||||
return 'success';
|
||||
};
|
||||
});
|
||||
|
|
|
@ -5,4 +5,5 @@ function SettingsViewModel(data) {
|
|||
this.LDAPSettings = data.LDAPSettings;
|
||||
this.AllowBindMountsForRegularUsers = data.AllowBindMountsForRegularUsers;
|
||||
this.AllowPrivilegedModeForRegularUsers = data.AllowPrivilegedModeForRegularUsers;
|
||||
this.SnapshotInterval = data.SnapshotInterval;
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
function StatusViewModel(data) {
|
||||
this.Authentication = data.Authentication;
|
||||
this.Snapshot = data.Snapshot;
|
||||
this.EndpointManagement = data.EndpointManagement;
|
||||
this.Analytics = data.Analytics;
|
||||
this.Version = data.Version;
|
||||
|
|
|
@ -25,12 +25,19 @@ function StateManagerFactory($q, SystemService, InfoHelper, LocalStorage, Settin
|
|||
LocalStorage.storeApplicationState(state.application);
|
||||
};
|
||||
|
||||
manager.updateSnapshotInterval = function(interval) {
|
||||
state.application.snapshotInterval = interval;
|
||||
LocalStorage.storeApplicationState(state.application);
|
||||
};
|
||||
|
||||
function assignStateFromStatusAndSettings(status, settings) {
|
||||
state.application.authentication = status.Authentication;
|
||||
state.application.analytics = status.Analytics;
|
||||
state.application.endpointManagement = status.EndpointManagement;
|
||||
state.application.snapshot = status.Snapshot;
|
||||
state.application.version = status.Version;
|
||||
state.application.logo = settings.LogoURL;
|
||||
state.application.snapshotInterval = settings.SnapshotInterval;
|
||||
state.application.validity = moment().unix();
|
||||
}
|
||||
|
||||
|
@ -110,14 +117,11 @@ function StateManagerFactory($q, SystemService, InfoHelper, LocalStorage, Settin
|
|||
return extensions;
|
||||
}
|
||||
|
||||
manager.updateEndpointState = function(loading, type, extensions) {
|
||||
manager.updateEndpointState = function(name, type, extensions) {
|
||||
var deferred = $q.defer();
|
||||
|
||||
if (loading) {
|
||||
state.loading = true;
|
||||
}
|
||||
|
||||
if (type === 3) {
|
||||
state.endpoint.name = name;
|
||||
state.endpoint.mode = { provider: 'AZURE' };
|
||||
LocalStorage.storeEndpointState(state.endpoint);
|
||||
deferred.resolve();
|
||||
|
@ -132,6 +136,7 @@ function StateManagerFactory($q, SystemService, InfoHelper, LocalStorage, Settin
|
|||
var endpointMode = InfoHelper.determineEndpointMode(data.info, type);
|
||||
var endpointAPIVersion = parseFloat(data.version.ApiVersion);
|
||||
state.endpoint.mode = endpointMode;
|
||||
state.endpoint.name = name;
|
||||
state.endpoint.apiVersion = endpointAPIVersion;
|
||||
state.endpoint.extensions = assignExtensions(extensions);
|
||||
LocalStorage.storeEndpointState(state.endpoint);
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
angular.module('portainer.app')
|
||||
.controller('AuthenticationController', ['$scope', '$state', '$transition$', '$window', '$timeout', '$sanitize', 'Authentication', 'Users', 'UserService', 'EndpointService', 'StateManager', 'EndpointProvider', 'Notifications', 'SettingsService', 'ExtensionManager',
|
||||
function ($scope, $state, $transition$, $window, $timeout, $sanitize, Authentication, Users, UserService, EndpointService, StateManager, EndpointProvider, Notifications, SettingsService, ExtensionManager) {
|
||||
.controller('AuthenticationController', ['$scope', '$state', '$transition$', '$sanitize', 'Authentication', 'UserService', 'EndpointService', 'StateManager', 'Notifications', 'SettingsService',
|
||||
function ($scope, $state, $transition$, $sanitize, Authentication, UserService, EndpointService, StateManager, Notifications, SettingsService) {
|
||||
|
||||
$scope.logo = StateManager.getState().application.logo;
|
||||
|
||||
|
@ -13,47 +13,13 @@ function ($scope, $state, $transition$, $window, $timeout, $sanitize, Authentica
|
|||
AuthenticationError: ''
|
||||
};
|
||||
|
||||
function redirectToDockerDashboard(endpoint) {
|
||||
ExtensionManager.initEndpointExtensions(endpoint.Id)
|
||||
.then(function success(data) {
|
||||
var extensions = data;
|
||||
return StateManager.updateEndpointState(true, endpoint.Type, extensions);
|
||||
})
|
||||
.then(function success(data) {
|
||||
$state.go('docker.dashboard');
|
||||
})
|
||||
.catch(function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to connect to the Docker endpoint');
|
||||
});
|
||||
}
|
||||
|
||||
function redirectToAzureDashboard(endpoint) {
|
||||
StateManager.updateEndpointState(false, endpoint.Type, [])
|
||||
.then(function success(data) {
|
||||
$state.go('azure.dashboard');
|
||||
})
|
||||
.catch(function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to connect to the Docker endpoint');
|
||||
});
|
||||
}
|
||||
|
||||
function redirectToDashboard(endpoint) {
|
||||
EndpointProvider.setEndpointID(endpoint.Id);
|
||||
|
||||
if (endpoint.Type === 3) {
|
||||
return redirectToAzureDashboard(endpoint);
|
||||
}
|
||||
redirectToDockerDashboard(endpoint);
|
||||
}
|
||||
|
||||
function unauthenticatedFlow() {
|
||||
EndpointService.endpoints()
|
||||
.then(function success(data) {
|
||||
var endpoints = data;
|
||||
if (endpoints.length > 0) {
|
||||
redirectToDashboard(endpoints[0]);
|
||||
} else {
|
||||
if (endpoints.length === 0) {
|
||||
$state.go('portainer.init.endpoint');
|
||||
} else {
|
||||
$state.go('portainer.home');
|
||||
}
|
||||
})
|
||||
.catch(function error(err) {
|
||||
|
@ -92,13 +58,10 @@ function ($scope, $state, $transition$, $window, $timeout, $sanitize, Authentica
|
|||
.then(function success(data) {
|
||||
var endpoints = data;
|
||||
var userDetails = Authentication.getUserDetails();
|
||||
if (endpoints.length > 0) {
|
||||
redirectToDashboard(endpoints[0]);
|
||||
} else if (endpoints.length === 0 && userDetails.role === 1) {
|
||||
if (endpoints.length === 0 && userDetails.role === 1) {
|
||||
$state.go('portainer.init.endpoint');
|
||||
} else if (endpoints.length === 0 && userDetails.role === 2) {
|
||||
Authentication.logout();
|
||||
$scope.state.AuthenticationError = 'User not allowed. Please contact your administrator.';
|
||||
} else {
|
||||
$state.go('portainer.home');
|
||||
}
|
||||
})
|
||||
.catch(function error() {
|
||||
|
@ -114,7 +77,7 @@ function ($scope, $state, $transition$, $window, $timeout, $sanitize, Authentica
|
|||
}
|
||||
|
||||
if (Authentication.isAuthenticated()) {
|
||||
$state.go('docker.dashboard');
|
||||
$state.go('portainer.home');
|
||||
}
|
||||
|
||||
var authenticationEnabled = $scope.applicationState.application.authentication;
|
||||
|
|
|
@ -0,0 +1,37 @@
|
|||
<rd-header>
|
||||
<rd-header-title title-text="Home">
|
||||
<a data-toggle="tooltip" title="Refresh" ui-sref="portainer.home" ui-sref-opts="{reload: true}">
|
||||
<i class="fa fa-sync" aria-hidden="true"></i>
|
||||
</a>
|
||||
</rd-header-title>
|
||||
<rd-header-content>Endpoints</rd-header-content>
|
||||
</rd-header>
|
||||
|
||||
<information-panel title-text="Information" ng-if="!isAdmin && endpoints.length === 0">
|
||||
<span class="small">
|
||||
<p class="text-muted">
|
||||
<i class="fa fa-exclamation-circle orange-icon" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||
You do not have access to any environment. Please contact your administrator.
|
||||
</p>
|
||||
</span>
|
||||
</information-panel>
|
||||
|
||||
<information-panel title-text="Information" ng-if="isAdmin && !applicationState.application.snapshot">
|
||||
<span class="small">
|
||||
<p class="text-muted">
|
||||
<i class="fa fa-exclamation-circle orange-icon" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||
Endpoint snapshot is disabled.
|
||||
</p>
|
||||
</span>
|
||||
</information-panel>
|
||||
|
||||
<div class="row" ng-if="endpoints.length > 0">
|
||||
<div class="col-sm-12">
|
||||
<endpoints-snapshot-datatable
|
||||
title-text="Endpoints" title-icon="fa-plug"
|
||||
dataset="endpoints" table-key="endpoints"
|
||||
order-by="Name"
|
||||
dashboard-action="goToDashboard"
|
||||
></endpoints-snapshot-datatable>
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,58 @@
|
|||
angular.module('portainer.app')
|
||||
.controller('HomeController', ['$q', '$scope', '$state', 'Authentication', 'EndpointService', 'EndpointHelper', 'GroupService', 'Notifications', 'EndpointProvider', 'StateManager', 'ExtensionManager',
|
||||
function ($q, $scope, $state, Authentication, EndpointService, EndpointHelper, GroupService, Notifications, EndpointProvider, StateManager, ExtensionManager) {
|
||||
|
||||
$scope.goToDashboard = function(endpoint) {
|
||||
EndpointProvider.setEndpointID(endpoint.Id);
|
||||
EndpointProvider.setEndpointPublicURL(endpoint.PublicURL);
|
||||
if (endpoint.Type === 3) {
|
||||
switchToAzureEndpoint(endpoint);
|
||||
} else {
|
||||
switchToDockerEndpoint(endpoint);
|
||||
}
|
||||
};
|
||||
|
||||
function switchToAzureEndpoint(endpoint) {
|
||||
StateManager.updateEndpointState(endpoint.Name, endpoint.Type, [])
|
||||
.then(function success() {
|
||||
$state.go('azure.dashboard');
|
||||
})
|
||||
.catch(function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to connect to the Azure endpoint');
|
||||
});
|
||||
}
|
||||
|
||||
function switchToDockerEndpoint(endpoint) {
|
||||
ExtensionManager.initEndpointExtensions(endpoint.Id)
|
||||
.then(function success(data) {
|
||||
var extensions = data;
|
||||
return StateManager.updateEndpointState(endpoint.Name, endpoint.Type, extensions);
|
||||
})
|
||||
.then(function success() {
|
||||
$state.go('docker.dashboard');
|
||||
})
|
||||
.catch(function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to connect to the Docker endpoint');
|
||||
});
|
||||
}
|
||||
|
||||
function initView() {
|
||||
$scope.isAdmin = Authentication.getUserDetails().role === 1;
|
||||
|
||||
$q.all({
|
||||
endpoints: EndpointService.endpoints(),
|
||||
groups: GroupService.groups()
|
||||
})
|
||||
.then(function success(data) {
|
||||
var endpoints = data.endpoints;
|
||||
var groups = data.groups;
|
||||
EndpointHelper.mapGroupNameToEndpoint(endpoints, groups);
|
||||
$scope.endpoints = endpoints;
|
||||
})
|
||||
.catch(function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to retrieve endpoint information');
|
||||
});
|
||||
}
|
||||
|
||||
initView();
|
||||
}]);
|
|
@ -1,6 +1,6 @@
|
|||
angular.module('portainer.app')
|
||||
.controller('InitAdminController', ['$scope', '$state', '$sanitize', 'Notifications', 'Authentication', 'StateManager', 'UserService', 'EndpointService', 'EndpointProvider', 'ExtensionManager',
|
||||
function ($scope, $state, $sanitize, Notifications, Authentication, StateManager, UserService, EndpointService, EndpointProvider, ExtensionManager) {
|
||||
.controller('InitAdminController', ['$scope', '$state', '$sanitize', 'Notifications', 'Authentication', 'StateManager', 'UserService', 'EndpointService',
|
||||
function ($scope, $state, $sanitize, Notifications, Authentication, StateManager, UserService, EndpointService) {
|
||||
|
||||
$scope.logo = StateManager.getState().application.logo;
|
||||
|
||||
|
@ -30,20 +30,7 @@ function ($scope, $state, $sanitize, Notifications, Authentication, StateManager
|
|||
if (data.length === 0) {
|
||||
$state.go('portainer.init.endpoint');
|
||||
} else {
|
||||
var endpoint = data[0];
|
||||
endpointID = endpoint.Id;
|
||||
EndpointProvider.setEndpointID(endpointID);
|
||||
ExtensionManager.initEndpointExtensions(endpointID)
|
||||
.then(function success(data) {
|
||||
var extensions = data;
|
||||
return StateManager.updateEndpointState(false, endpoint.Type, extensions);
|
||||
})
|
||||
.then(function success() {
|
||||
$state.go('docker.dashboard');
|
||||
})
|
||||
.catch(function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to connect to Docker environment');
|
||||
});
|
||||
$state.go('portainer.home');
|
||||
}
|
||||
})
|
||||
.catch(function error(err) {
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
angular.module('portainer.app')
|
||||
.controller('InitEndpointController', ['$scope', '$state', 'EndpointService', 'StateManager', 'EndpointProvider', 'Notifications', 'ExtensionManager',
|
||||
function ($scope, $state, EndpointService, StateManager, EndpointProvider, Notifications, ExtensionManager) {
|
||||
.controller('InitEndpointController', ['$scope', '$state', 'EndpointService', 'StateManager', 'Notifications',
|
||||
function ($scope, $state, EndpointService, StateManager, Notifications) {
|
||||
|
||||
if (!_.isEmpty($scope.applicationState.endpoint)) {
|
||||
$state.go('docker.dashboard');
|
||||
$state.go('portainer.home');
|
||||
}
|
||||
|
||||
$scope.logo = StateManager.getState().application.logo;
|
||||
|
@ -36,16 +36,7 @@ function ($scope, $state, EndpointService, StateManager, EndpointProvider, Notif
|
|||
$scope.state.actionInProgress = true;
|
||||
EndpointService.createLocalEndpoint()
|
||||
.then(function success(data) {
|
||||
endpoint = data;
|
||||
EndpointProvider.setEndpointID(endpoint.Id);
|
||||
return ExtensionManager.initEndpointExtensions(endpoint.Id);
|
||||
})
|
||||
.then(function success(data) {
|
||||
var extensions = data;
|
||||
return StateManager.updateEndpointState(false, endpoint.Type, extensions);
|
||||
})
|
||||
.then(function success(data) {
|
||||
$state.go('docker.dashboard');
|
||||
$state.go('portainer.home');
|
||||
})
|
||||
.catch(function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to connect to the Docker environment');
|
||||
|
@ -92,12 +83,7 @@ function ($scope, $state, EndpointService, StateManager, EndpointProvider, Notif
|
|||
$scope.state.actionInProgress = true;
|
||||
EndpointService.createAzureEndpoint(name, applicationId, tenantId, authenticationKey, 1, [])
|
||||
.then(function success(data) {
|
||||
endpoint = data;
|
||||
EndpointProvider.setEndpointID(endpoint.Id);
|
||||
return StateManager.updateEndpointState(false, endpoint.Type, []);
|
||||
})
|
||||
.then(function success(data) {
|
||||
$state.go('azure.dashboard');
|
||||
$state.go('portainer.home');
|
||||
})
|
||||
.catch(function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to connect to the Azure environment');
|
||||
|
@ -112,16 +98,7 @@ function ($scope, $state, EndpointService, StateManager, EndpointProvider, Notif
|
|||
$scope.state.actionInProgress = true;
|
||||
EndpointService.createRemoteEndpoint(name, type, URL, PublicURL, 1, [], TLS, TLSSkipVerify, TLSSKipClientVerify, TLSCAFile, TLSCertFile, TLSKeyFile)
|
||||
.then(function success(data) {
|
||||
endpoint = data;
|
||||
EndpointProvider.setEndpointID(endpoint.Id);
|
||||
return ExtensionManager.initEndpointExtensions(endpoint.Id);
|
||||
})
|
||||
.then(function success(data) {
|
||||
var extensions = data;
|
||||
return StateManager.updateEndpointState(false, endpoint.Type, extensions);
|
||||
})
|
||||
.then(function success(data) {
|
||||
$state.go('docker.dashboard');
|
||||
$state.go('portainer.home');
|
||||
})
|
||||
.catch(function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to connect to the Docker environment');
|
||||
|
|
|
@ -9,6 +9,14 @@
|
|||
<rd-widget-header icon="fa-cogs" title-text="Application settings"></rd-widget-header>
|
||||
<rd-widget-body>
|
||||
<form class="form-horizontal">
|
||||
<!-- snapshot-interval -->
|
||||
<div class="form-group">
|
||||
<label for="snapshot_interval" class="col-sm-2 control-label text-left">Snapshot interval</label>
|
||||
<div class="col-sm-10">
|
||||
<input type="text" class="form-control" ng-model="settings.SnapshotInterval" id="snapshot_interval" placeholder="e.g. 15m">
|
||||
</div>
|
||||
</div>
|
||||
<!-- !snapshot-interval -->
|
||||
<!-- logo -->
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
|
|
|
@ -51,6 +51,7 @@ function ($scope, $state, Notifications, SettingsService, StateManager, DEFAULT_
|
|||
.then(function success(data) {
|
||||
Notifications.success('Settings updated');
|
||||
StateManager.updateLogo(settings.LogoURL);
|
||||
StateManager.updateSnapshotInterval(settings.SnapshotInterval);
|
||||
$state.reload();
|
||||
})
|
||||
.catch(function error(err) {
|
||||
|
|
|
@ -1,35 +1,32 @@
|
|||
<!-- Sidebar -->
|
||||
<div id="sidebar-wrapper">
|
||||
<div class="sidebar-header">
|
||||
<a ng-click="toggleSidebar()" class="interactive">
|
||||
<a ui-sref="portainer.home">
|
||||
<img ng-if="logo" ng-src="{{ logo }}" class="img-responsive logo">
|
||||
<img ng-if="!logo" src="images/logo.png" class="img-responsive logo" alt="Portainer">
|
||||
<span class="menu-icon glyphicon glyphicon-transfer"></span>
|
||||
</a>
|
||||
<a ng-click="toggleSidebar()"><span class="menu-icon glyphicon glyphicon-transfer"></span></a>
|
||||
</div>
|
||||
<div class="sidebar-content">
|
||||
<ul class="sidebar">
|
||||
<sidebar-endpoint-selector ng-if="endpoints && groups"
|
||||
endpoints="endpoints"
|
||||
groups="groups"
|
||||
select-endpoint="switchEndpoint"
|
||||
></sidebar-endpoint-selector>
|
||||
<li class="sidebar-title"><span class="endpoint-name">{{ activeEndpoint.Name }}</span></li>
|
||||
<azure-sidebar-content ng-if="applicationState.endpoint.mode.provider === 'AZURE'">
|
||||
<li class="sidebar-list">
|
||||
<a ui-sref="portainer.home" ui-sref-active="active">Home <span class="menu-icon fa fa-home fa-fw"></span></a>
|
||||
</li>
|
||||
<li class="sidebar-title endpoint-name" ng-if="applicationState.endpoint.name">
|
||||
<span class="fa fa-plug space-right"></span>{{ applicationState.endpoint.name }}
|
||||
</li>
|
||||
<azure-sidebar-content ng-if="applicationState.endpoint.mode && applicationState.endpoint.mode.provider === 'AZURE'">
|
||||
</azure-sidebar-content>
|
||||
<docker-sidebar-content ng-if="applicationState.endpoint.mode.provider !== 'AZURE'"
|
||||
<docker-sidebar-content ng-if="applicationState.endpoint.mode && applicationState.endpoint.mode.provider !== 'AZURE'"
|
||||
endpoint-api-version="applicationState.endpoint.apiVersion"
|
||||
swarm-management="applicationState.endpoint.mode.provider === 'DOCKER_SWARM_MODE' && applicationState.endpoint.mode.role === 'MANAGER'"
|
||||
standalone-management="applicationState.endpoint.mode.provider === 'DOCKER_STANDALONE' || applicationState.endpoint.mode.provider === 'VMWARE_VIC'"
|
||||
admin-access="!applicationState.application.authentication || isAdmin"
|
||||
external-contributions="displayExternalContributors"
|
||||
sidebar-toggled-on="toggle"
|
||||
current-state="$state.current.name"
|
||||
></docker-sidebar-content>
|
||||
<li class="sidebar-title" ng-if="applicationState.endpoint.extensions.length > 0">
|
||||
<li class="sidebar-title" ng-if="applicationState.endpoint.mode && applicationState.endpoint.extensions.length > 0">
|
||||
<span>Extensions</span>
|
||||
</li>
|
||||
<li class="sidebar-list" ng-if="applicationState.endpoint.extensions.indexOf('storidge') !== -1 && applicationState.endpoint.mode.provider === 'DOCKER_SWARM_MODE' && applicationState.endpoint.mode.role === 'MANAGER'">
|
||||
<li class="sidebar-list" ng-if="applicationState.endpoint.mode && applicationState.endpoint.extensions.indexOf('storidge') !== -1 && applicationState.endpoint.mode.provider === 'DOCKER_SWARM_MODE' && applicationState.endpoint.mode.role === 'MANAGER'">
|
||||
<a ui-sref="storidge.cluster" ui-sref-active="active">Storidge <span class="menu-icon fa fa-bolt fa-fw"></span></a>
|
||||
<div class="sidebar-sublist" ng-if="toggle && ($state.current.name === 'storidge.cluster' || $state.current.name === 'storidge.profiles' || $state.current.name === 'storidge.monitor' || $state.current.name === 'storidge.profiles.new' || $state.current.name === 'storidge.profiles.profile')">
|
||||
<a ui-sref="storidge.monitor" ui-sref-active="active">Monitor</a>
|
||||
|
@ -39,10 +36,10 @@
|
|||
</div>
|
||||
</li>
|
||||
<li class="sidebar-title" ng-if="!applicationState.application.authentication || isAdmin || isTeamLeader">
|
||||
<span>Portainer settings</span>
|
||||
<span>Settings</span>
|
||||
</li>
|
||||
<li class="sidebar-list" ng-if="applicationState.application.authentication && (isAdmin || isTeamLeader)">
|
||||
<a ui-sref="portainer.users" ui-sref-active="active">User management <span class="menu-icon fa fa-users fa-fw"></span></a>
|
||||
<a ui-sref="portainer.users" ui-sref-active="active">Users <span class="menu-icon fa fa-users fa-fw"></span></a>
|
||||
<div class="sidebar-sublist" ng-if="toggle && ($state.current.name === 'portainer.users' || $state.current.name === 'portainer.users.user' || $state.current.name === 'portainer.teams' || $state.current.name === 'portainer.teams.team')">
|
||||
<a ui-sref="portainer.teams" ui-sref-active="active">Teams</a>
|
||||
</div>
|
||||
|
|
|
@ -1,64 +1,6 @@
|
|||
angular.module('portainer.app')
|
||||
.controller('SidebarController', ['$q', '$scope', '$state', 'EndpointService', 'GroupService', 'StateManager', 'EndpointProvider', 'Notifications', 'Authentication', 'UserService', 'ExtensionManager',
|
||||
function ($q, $scope, $state, EndpointService, GroupService, StateManager, EndpointProvider, Notifications, Authentication, UserService, ExtensionManager) {
|
||||
|
||||
$scope.switchEndpoint = function(endpoint) {
|
||||
EndpointProvider.setEndpointID(endpoint.Id);
|
||||
EndpointProvider.setEndpointPublicURL(endpoint.PublicURL);
|
||||
if (endpoint.Type === 3) {
|
||||
switchToAzureEndpoint(endpoint);
|
||||
} else {
|
||||
switchToDockerEndpoint(endpoint);
|
||||
}
|
||||
};
|
||||
|
||||
function switchToAzureEndpoint(endpoint) {
|
||||
StateManager.updateEndpointState(false, endpoint.Type, [])
|
||||
.then(function success() {
|
||||
$scope.currentEndpoint = endpoint;
|
||||
$state.go('azure.dashboard');
|
||||
})
|
||||
.catch(function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to connect to the Docker endpoint');
|
||||
var currentEndpoint = $scope.currentEndpoint;
|
||||
EndpointProvider.setEndpointID(currentEndpoint.Id);
|
||||
EndpointProvider.setEndpointPublicURL(currentEndpoint.PublicURL);
|
||||
return StateManager.updateEndpointState(true, currentEndpoint.Type, currentEndpoint.Extensions);
|
||||
});
|
||||
}
|
||||
|
||||
function switchToDockerEndpoint(endpoint) {
|
||||
ExtensionManager.initEndpointExtensions(endpoint.Id)
|
||||
.then(function success(data) {
|
||||
var extensions = data;
|
||||
return StateManager.updateEndpointState(true, endpoint.Type, extensions);
|
||||
})
|
||||
.then(function success() {
|
||||
$scope.currentEndpoint = endpoint;
|
||||
$state.go('docker.dashboard');
|
||||
})
|
||||
.catch(function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to connect to the Docker endpoint');
|
||||
var currentEndpoint = $scope.currentEndpoint;
|
||||
EndpointProvider.setEndpointID(currentEndpoint.Id);
|
||||
EndpointProvider.setEndpointPublicURL(currentEndpoint.PublicURL);
|
||||
return StateManager.updateEndpointState(true, currentEndpoint.Type, currentEndpoint.Extensions);
|
||||
});
|
||||
}
|
||||
|
||||
function setActiveEndpoint(endpoints) {
|
||||
var activeEndpointID = EndpointProvider.endpointID();
|
||||
|
||||
for (var i = 0; i < endpoints.length; i++) {
|
||||
var endpoint = endpoints[i];
|
||||
if (endpoint.Id === activeEndpointID) {
|
||||
$scope.activeEndpoint = endpoint;
|
||||
$scope.currentEndpoint = endpoint;
|
||||
EndpointProvider.setEndpointPublicURL(endpoint.PublicURL);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
.controller('SidebarController', ['$q', '$scope', 'StateManager', 'Notifications', 'Authentication', 'UserService',
|
||||
function ($q, $scope, StateManager, Notifications, Authentication, UserService) {
|
||||
|
||||
function checkPermissions(memberships) {
|
||||
var isLeader = false;
|
||||
|
@ -72,32 +14,18 @@ function ($q, $scope, $state, EndpointService, GroupService, StateManager, Endpo
|
|||
|
||||
function initView() {
|
||||
$scope.uiVersion = StateManager.getState().application.version;
|
||||
$scope.displayExternalContributors = StateManager.getState().application.displayExternalContributors;
|
||||
$scope.logo = StateManager.getState().application.logo;
|
||||
|
||||
$q.all({
|
||||
endpoints: EndpointService.endpoints(),
|
||||
groups: GroupService.groups()
|
||||
})
|
||||
.then(function success(data) {
|
||||
var endpoints = data.endpoints;
|
||||
$scope.groups = _.sortBy(data.groups, ['Name']);
|
||||
$scope.endpoints = _.sortBy(endpoints, ['Name']);
|
||||
var userDetails = Authentication.getUserDetails();
|
||||
var isAdmin = userDetails.role === 1;
|
||||
$scope.isAdmin = isAdmin;
|
||||
|
||||
setActiveEndpoint(endpoints);
|
||||
|
||||
if (StateManager.getState().application.authentication) {
|
||||
var userDetails = Authentication.getUserDetails();
|
||||
var isAdmin = userDetails.role === 1 ? true: false;
|
||||
$scope.isAdmin = isAdmin;
|
||||
return $q.when(!isAdmin ? UserService.userMemberships(userDetails.ID) : []);
|
||||
}
|
||||
})
|
||||
$q.when(!isAdmin ? UserService.userMemberships(userDetails.ID) : [])
|
||||
.then(function success(data) {
|
||||
checkPermissions(data);
|
||||
})
|
||||
.catch(function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to retrieve endpoints');
|
||||
Notifications.error('Failure', err, 'Unable to retrieve user memberships');
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -311,8 +311,10 @@ ul.sidebar .sidebar-title {
|
|||
height: auto;
|
||||
}
|
||||
|
||||
ul.sidebar .sidebar-title .endpoint-name {
|
||||
ul.sidebar .sidebar-title.endpoint-name {
|
||||
color: #fff;
|
||||
text-align: center;
|
||||
text-indent: 0;
|
||||
}
|
||||
|
||||
ul.sidebar .sidebar-list a {
|
||||
|
|
|
@ -23,7 +23,7 @@
|
|||
"node": ">= 0.8.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-free-webfonts": "^1.0.4",
|
||||
"@fortawesome/fontawesome-free-webfonts": "^1.0.9",
|
||||
"@uirouter/angularjs": "~1.0.6",
|
||||
"angular": "~1.5.0",
|
||||
"angular-clipboard": "^1.6.2",
|
||||
|
|
|
@ -2,9 +2,9 @@
|
|||
# yarn lockfile v1
|
||||
|
||||
|
||||
"@fortawesome/fontawesome-free-webfonts@^1.0.4":
|
||||
version "1.0.4"
|
||||
resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-free-webfonts/-/fontawesome-free-webfonts-1.0.4.tgz#bac5d89755bf3bc2d2b4deee47d92febf641bb1f"
|
||||
"@fortawesome/fontawesome-free-webfonts@^1.0.9":
|
||||
version "1.0.9"
|
||||
resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-free-webfonts/-/fontawesome-free-webfonts-1.0.9.tgz#72f2c10453422aba0d338fa6a9cb761b50ba24d5"
|
||||
|
||||
"@uirouter/angularjs@~1.0.6":
|
||||
version "1.0.11"
|
||||
|
|
Loading…
Reference in New Issue